485 lines
16 KiB
TypeScript
485 lines
16 KiB
TypeScript
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<HTMLCanvasElement>;
|
|
@ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef<HTMLCanvasElement>;
|
|
@ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef<HTMLCanvasElement>;
|
|
@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()];
|
|
}
|
|
|
|
}
|