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