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, formatDate } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { provideNativeDateAdapter} from '@angular/material/core'; import { MatDatepickerModule} from '@angular/material/datepicker'; import { MatFormFieldModule} from '@angular/material/form-field'; import { format } from 'date-fns'; import { ToastrService } from 'ngx-toastr'; 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, MatDatepickerModule, MatFormFieldModule], providers: [provideNativeDateAdapter()], 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'; startDate: Date | null = null; endDate: Date | null = null; 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; private updateTimeout: any; constructor(private sensorService: SensorService, private toast: ToastrService) {} ngOnInit(): void { this.resizeListener = this.onResize.bind(this); window.addEventListener('resize', this.resizeListener); } ngOnChanges(changes: SimpleChanges): void { if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) { this.selectedInterval = changes['interval'].currentValue; } } ngAfterViewInit(): void { this.onResize(); } dateChange(): void { if(!this.startDate || !this.endDate){ this.toast.warning('Select both start and end dates'); return; } else { clearTimeout(this.updateTimeout); this.updateTimeout = setTimeout(() => { this.updateCharts(); }, 500); } } ngOnDestroy(): void { window.removeEventListener('resize', this.resizeListener); } onResize(): void { Object.values(this.charts).forEach(chart => { if (chart) { chart.destroy(); } }); this.updateCharts(); } formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } 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'); const hasil = `${year}-${month}-${day}`; return hasil; } 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, startDate?: Date, endDate?: Date): void { const hStart = startDate ? this.formatDate(startDate) : this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate(); const dStart= startDate ? this.formatDate(startDate) : this.getDateAgo(); const dEnd = endDate ? this.formatDate(endDate) : this.getDate(); 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, startDate?: Date, endDate?: Date): void { const hStart = startDate ? this.formatDate(startDate) : this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate(); const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo(); const dEnd= endDate ? this.formatDate(endDate) : this.getDate(); if(timeRange === 'HOURLY'){ this.sensorService.getSensorDataHourly('npk1', this.selectedNPK1, hStart, hEnd, 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, startDate?: Date, endDate?: Date): void { const hStart = startDate ? this.formatDate(startDate) : this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate(); const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo(); const dEnd = endDate ? this.formatDate(endDate) : this.getDate(); 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].data.labels = []; this.charts[key].data.datasets = []; this.charts[key] = undefined; } }); if (this.startDate && this.endDate) { this.fetchDHTData(interval, this.startDate, this.endDate); this.fetchNPK1Data(interval, this.startDate, this.endDate); this.fetchNPK2Data(interval, this.startDate, this.endDate); } else { 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: any) => { 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: string | number, index: number, values: any) => { 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.date); labels.push(item.hour); } 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 => { const formatDate = format(new Date(item.date), 'MMM dd'); if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') { return `${formatDate}, ${item.hour}.00`; } else if (this.interval === 'DAILY') { const day = item.date; const convertDateToDay = this.convertDateToDay(day); return `${convertDateToDay}, ${formatDate}`; } else { return ''; } }); } convertDateToDay(dateString: string): string { const date = new Date(dateString); const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thrus', 'Fri', 'Sat']; return days[date.getDay()]; } }