import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnChanges, Input, SimpleChanges, ChangeDetectorRef } from '@angular/core'; import { Chart, registerables } from 'chart.js'; import { SensorService } from '../../../../cores/services/sensor.service'; import { ApiResponse, ParameterSensor } from '../../../../cores/interface/sensor-data'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; Chart.register(...registerables); const parameterColors: { [key: string]: string } = { vicitemperature: '#8F5A62', vicihumidity: '#16423C', viciluminosity: '#DF9B55', soiltemperature: '#FF6347', soilhumidity: '#0389b5', soilconductivity: '#A52A2A', soilph: '#228B22', soilnitrogen: '#fece48', soilphosphorus: '#B80F0A', soilpotassium: '#4c1f74', }; @Component({ selector: 'app-graph', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './graph.component.html', styleUrls: ['./graph.component.scss'] }) export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges{ @ViewChild('myChartDHT', { static: false }) dhtChartElement!: ElementRef; @ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef; @ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef; @Input() interval: string = 'hourly'; selectedInterval: string = 'HOURLY'; isLoadingDHT: boolean = true; isLoadingNPK1: boolean = true; isLoadingNPK2: boolean = true; isNoDataDHT: boolean = false; isNoDataNPK1: boolean = false; isNoDataNPK2: boolean = false; allNoData: boolean = false; sensorParameters: { [key: string]: string[] } = { dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'], npk1: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'], npk2: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'], }; parameterDisplayNames: { [key: string]: string } = { vicitemperature: 'Temperatur Udara (°C)', vicihumidity: 'Kelembaban Udara (%)', viciluminosity: 'Intensitas Cahaya (lux)', soiltemperature: 'Temperatur Tanah (°C)', soilhumidity: 'Kelembaban Tanah (%)', soilconductivity: 'Conductivity (μS/cm)', soilph: 'pH', soilnitrogen: 'Nitrogen (mg/l)', soilphosphorus: 'Phosphorus (mg/l)', soilpotassium: 'Kalium (mg/l)', }; charts: { [key: string]: Chart | undefined } = { dht: undefined, npk1: undefined, npk2: undefined, }; selectedNPK1: string = 'npk'; selectedNPK2: string = 'npk'; private resizeListener!: () => void; constructor(private sensorService: SensorService, private cdr: ChangeDetectorRef) {} ngOnInit(): void { this.resizeListener = this.onResize.bind(this); window.addEventListener('resize', this.resizeListener); this.updateCharts(); } ngOnChanges(changes: SimpleChanges): void { if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) { this.selectedInterval = changes['interval'].currentValue; } } ngAfterViewInit(): void { this.onResize(); } ngOnDestroy(): void { window.removeEventListener('resize', this.resizeListener); } onResize(): void { Object.values(this.charts).forEach(chart => { if (chart) { chart.destroy(); } }); this.updateCharts(); } getDate(): string { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } getDateAgo():string{ const today = new Date(); today.setDate(today.getDate() - 7); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } fetchDHTData(timeRange: string): void { const hEnd = this.getDate(); const hStart = this.getDate(); const dEnd = this.getDate(); const dStart = this.getDateAgo(); if(timeRange === 'HOURLY'){ this.sensorService.getSensorDataHourly('dht', 'npk', hStart, hEnd, timeRange).subscribe({ next: (response) => { this.isLoadingDHT = false; if (response.statusCode === 200 && response.data.dht?.length > 0) { this.createChart(this.dhtChartElement.nativeElement, response, 'dht', 'npk'); this.isNoDataDHT = false; } else { this.isNoDataDHT = true; } }, error: () => { this.isLoadingDHT = false; this.isNoDataDHT = true; } }); } else if(timeRange === 'DAILY'){ this.sensorService.getSensorDataDaily('dht', 'npk', dStart, dEnd, timeRange).subscribe({ next: (response) => { this.isLoadingDHT = false; if (response.statusCode === 200 && response.data.dht?.length > 0) { this.createChart(this.dhtChartElement.nativeElement, response, 'dht', 'npk'); this.isNoDataDHT = false; } else { this.isNoDataDHT = true; } }, error: () => { this.isLoadingDHT = false; this.isNoDataDHT = true; } }); } } fetchNPK1Data(timeRange: string): void { const hEnd = this.getDate(); const hStart = this.getDate(); const dEnd = this.getDate(); const dStart = this.getDateAgo(); if(timeRange === 'HOURLY'){ this.sensorService.getSensorDataHourly('npk1', this.selectedNPK1, hEnd, hStart, timeRange).subscribe({ next: (response) => { this.isLoadingNPK1 = false; if (response.statusCode === 200 && response.data.npk1?.length > 0) { this.createChart(this.npk1ChartElement.nativeElement, response, 'npk1', this.selectedNPK1); this.isNoDataNPK1 = false; } else { this.isNoDataNPK1 = true; } }, error: () => { this.isLoadingNPK1 = false; this.isNoDataNPK1 = true; } }) }else if(timeRange === 'DAILY'){ this.sensorService.getSensorDataDaily('npk1', this.selectedNPK1, dStart, dEnd, timeRange).subscribe({ next: (response) => { this.isLoadingNPK1 = false; if (response.statusCode === 200 && response.data.npk1?.length > 0) { this.createChart(this.npk1ChartElement.nativeElement, response, 'npk1', this.selectedNPK1); this.isNoDataNPK1 = false; } else { this.isNoDataNPK1 = true; } }, error: () => { this.isLoadingNPK1 = false; this.isNoDataNPK1 = true; } }); } } fetchNPK2Data(savedTimeRange: string): void { const hEnd = this.getDate(); const hStart = this.getDate(); const dEnd = this.getDate(); const dStart = this.getDateAgo(); if(savedTimeRange === 'HOURLY'){ this.sensorService.getSensorDataHourly('npk2', this.selectedNPK2, hStart, hEnd, savedTimeRange).subscribe({ next: (response) => { this.isLoadingNPK2 = false; if (response.statusCode === 200 && response.data.npk2?.length > 0) { this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2); this.isNoDataNPK2 = false; } else { this.isNoDataNPK2 = true; } }, error: () => { this.isLoadingNPK2 = false; this.isNoDataNPK2 = true; } }); } else if(savedTimeRange === 'DAILY'){ this.sensorService.getSensorDataDaily('npk2', this.selectedNPK2, dStart, dEnd, savedTimeRange).subscribe({ next: (response) => { this.isLoadingNPK2 = false; if (response.statusCode === 200 && response.data.npk2?.length > 0) { this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2); this.isNoDataNPK2 = false; } else { this.isNoDataNPK2 = true; } }, error: () => { this.isLoadingNPK2 = false; this.isNoDataNPK2 = true; } }); } } updateCharts(): void { const interval = this.selectedInterval; Object.keys(this.charts).forEach(key => { if (this.charts[key]) { this.charts[key]?.destroy(); this.charts[key] = undefined; } }); this.fetchDHTData(interval); this.fetchNPK1Data(interval); this.fetchNPK2Data(interval); } createChart(canvas: HTMLCanvasElement, response: ApiResponse, sensor: string, selectedOption: string): void { const ctx = canvas.getContext('2d'); const parameters = this.sensorParameters[sensor]; if (!ctx) { console.error('Failed to get canvas context for sensor:', sensor); return; } let datasets: any[] = []; if (sensor === 'dht') { datasets = ['vicitemperature', 'viciluminosity', 'vicihumidity'].map(parameter => { const { data, labels } = this.getDataFromResponse(response, sensor, parameter); if (data.length === 0) { console.warn(`No data found for parameter: ${parameter}`); return null; } const displayName = this.parameterDisplayNames[parameter] || parameter; const borderColor = parameterColors[parameter] || '#000000'; const backgroundColor = `${borderColor}4D`; const pointRadius = data.length === 1 ? 5 : 0; const pointHoverRadius = data.length === 1 ? 7 : 0; return { label: displayName, data, borderColor, borderWidth: 1.5, fill: true, backgroundColor, tension: 0.5, pointRadius, pointHoverRadius, }; }).filter(dataset => dataset !== null); } else { datasets = parameters .filter(parameter => { if (selectedOption === 'npk') { return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter); } else if (selectedOption === 'others') { return !['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter); } return true; }) .map(parameter => { const { data, labels } = this.getDataFromResponse(response, sensor, parameter); if (data.length === 0) { console.warn(`No data found for parameter: ${parameter}`); return null; } const displayName = this.parameterDisplayNames[parameter] || parameter; const borderColor = parameterColors[parameter] || '#000000'; const backgroundColor = `${borderColor}4D`; const pointRadius = data.length === 1 ? 5 : 0; const pointHoverRadius = data.length === 1 ? 7 : 0; return { label: displayName, data, borderColor, borderWidth: 1.5, fill: true, backgroundColor, tension: 0.5, pointRadius, pointHoverRadius, }; }) .filter(dataset => dataset !== null); } if (datasets.length === 0) { console.warn('No valid datasets to render for sensor:', sensor); return; } if (this.charts[sensor]) { this.charts[sensor]?.destroy(); } this.charts[sensor] = new Chart(ctx, { type: 'line', data: { labels: this.getLabels(response, sensor), datasets, }, options: { responsive: true, maintainAspectRatio: false, aspectRatio: 2, plugins: { tooltip: { enabled: true, mode: 'nearest', intersect: false, callbacks: { label: (tooltipItem) => { const paramLabel = tooltipItem.dataset.label; const value = tooltipItem.formattedValue; return `${paramLabel}: ${value}`; } } }, legend: { display: true } }, scales: { x: { grid: { display: false }, beginAtZero: true, ticks: { callback: (value, index, values) => { const labels = this.getLabels(response, sensor); return labels[index] || value; } } }, y: { grid: { display: false }, beginAtZero: true } } } }); } getDataFromResponse(response: ApiResponse, sensor: string, parameter: string): { data: number[], labels: string[] } { const sensorData = response.data[sensor as keyof typeof response.data]; const data: number[] = []; const labels: string[] = []; if (sensorData) { sensorData.forEach(item => { data.push(item[parameter as keyof ParameterSensor] ?? 0); if(this.interval === 'HOURLY') { labels.push(`${item.hour}.00`); } else if (this.interval === 'DAILY') { labels.push(item.day); } }); } else { console.error('No data found for sensor:', sensor); } return { data, labels }; } getLabels(response: ApiResponse, sensor: string): string[] { const sensorData = response.data[sensor as keyof typeof response.data]; return sensorData.map(item => { if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') { return `${item.hour}.00`; } else if (this.interval === 'DAILY') { const day = item.day; return this.convertDateToDay(day); } else { return ''; } }); } convertDateToDay(dateString: string): string { const date = new Date(dateString); const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thrusday', 'Friday', 'Saturday']; return days[date.getDay()]; } }