feat(historygraphs): adding history graph in hourly and daily

This commit is contained in:
Desy Ayurianti 2024-11-06 05:42:46 +07:00
parent 2864560c56
commit 4bb2c107d3
9 changed files with 465 additions and 135 deletions

View File

@ -4,6 +4,7 @@ import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component';
import { AuthComponent } from './pages/auth/auth.component';
import { AuthGuard } from './cores/guards/auth.guard';
import { RegisterComponent } from './pages/register/register.component';
import { HistorygraphComponent } from './pages/dashboard/page/historygraph/historygraph.component';
export const routes: Routes = [
{
@ -28,6 +29,11 @@ export const routes: Routes = [
component: DashboardComponent,
canActivate: [AuthGuard]
},
{
path: 'historygraph',
component: HistorygraphComponent,
canActivate: [AuthGuard],
}
]
}
];

View File

@ -1,5 +1,6 @@
export interface ParameterSensor {
hour: number;
day: number;
//for DHT sensor
vicitemperature?: number;

View File

@ -7,10 +7,10 @@
<a routerLink='/dashboard' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="bi bi-graph-up title"></i> <span class="ms-1 d-none d-sm-inline menu">Dashboard</span> </a>
</li>
<!-- <li>
<a routerLink='/graph' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="bi bi-graph-up title"></i> <span class="ms-1 d-none d-sm-inline menu">Graph</span> </a>
</li> -->
<li>
<a routerLink='/historygraph' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="bi bi-file-earmark-text title"></i> <span class="ms-1 d-none d-sm-inline menu">History Graph</span> </a>
</li>
</ul>
<hr>

View File

@ -1,11 +1,13 @@
import { Component } from '@angular/core';
import { AuthService } from '../../../../cores/services/auth.service';
import { Router } from '@angular/router';
import { Router, RouterModule } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [RouterModule],
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
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';
@ -27,10 +27,12 @@ const parameterColors: { [key: string]: string } = {
templateUrl: './graph.component.html',
styleUrls: ['./graph.component.scss']
})
export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
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;
@ -72,7 +74,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
private resizeListener!: () => void;
constructor(private sensorService: SensorService) {}
constructor(private sensorService: SensorService, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.resizeListener = this.onResize.bind(this);
@ -80,6 +82,15 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateCharts();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['interval'] && !changes['interval'].firstChange) {
this.selectedInterval = changes['interval'].currentValue;
this.updateCharts();
}
}
ngAfterViewInit(): void {
this.onResize();
}
@ -97,27 +108,18 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateCharts();
}
updateCharts(): void {
this.isLoadingDHT = this.isLoadingNPK1 = this.isLoadingNPK2 = true;
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 startEnd = `${year}-${month}-${day}`;
const timeRange = 'HOURLY';
Object.keys(this.charts).forEach(key => {
if (this.charts[key]) {
this.charts[key]?.destroy();
this.charts[key] = undefined;
return `${year}-${month}-${day}`;
}
});
// Fetch data for DHT
this.sensorService.getSensorData('dht', '', startEnd, timeRange).subscribe({
fetchDHTData(timeRange: string): void {
const startEnd = this.getDate();
this.sensorService.getSensorData('dht', 'npk', startEnd, timeRange).subscribe({
next: (response) => {
this.isLoadingDHT = false;
if (response.statusCode === 200 && response.data.dht?.length > 0) {
@ -132,10 +134,11 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.isNoDataDHT = true;
}
});
}
// Fetch data for NPK1
this.sensorService.getSensorData('npk1', '', startEnd, timeRange).subscribe({
fetchNPK1Data(timeRange: string): void {
const startEnd = this.getDate();
this.sensorService.getSensorData('npk1', this.selectedNPK1, startEnd, timeRange).subscribe({
next: (response) => {
this.isLoadingNPK1 = false;
if (response.statusCode === 200 && response.data.npk1?.length > 0) {
@ -150,10 +153,14 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.isNoDataNPK1 = true;
}
});
}
// Fetch data for NPK2
this.sensorService.getSensorData('npk2', '', startEnd, timeRange).subscribe({
fetchNPK2Data(savedTimeRange: string): void {
const startEnd = this.getDate();
const timeRange = this.interval;
this.sensorService.getSensorData('npk2', this.selectedNPK2, startEnd, savedTimeRange).subscribe({
next: (response) => {
console.log(savedTimeRange);
this.isLoadingNPK2 = false;
if (response.statusCode === 200 && response.data.npk2?.length > 0) {
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
@ -169,6 +176,21 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
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 {
@ -195,6 +217,9 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
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,
@ -203,8 +228,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
fill: true,
backgroundColor,
tension: 0.5,
pointRadius: 0,
pointHoverRadius: 0,
pointRadius,
pointHoverRadius,
};
}).filter(dataset => dataset !== null);
} else {
@ -229,6 +254,9 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
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,
@ -237,8 +265,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
fill: true,
backgroundColor,
tension: 0.5,
pointRadius: 0,
pointHoverRadius: 0,
pointRadius,
pointHoverRadius,
};
})
.filter(dataset => dataset !== null);
@ -253,6 +281,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.charts[sensor]?.destroy();
}
this.charts[sensor] = new Chart(ctx, {
type: 'line',
data: {
@ -279,11 +308,21 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
legend: { display: true }
},
scales: {
x: { grid: { display: false }, beginAtZero: true },
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[] } {
@ -295,7 +334,12 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
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);
@ -306,6 +350,23 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
getLabels(response: ApiResponse, sensor: string): string[] {
const sensorData = response.data[sensor as keyof typeof response.data];
return sensorData ? sensorData.map(item => `${item.hour}.00`) : [];
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()];
}
}

View File

@ -0,0 +1,19 @@
<div class="container">
<div>
<h1 class="title">{{ greeting }}</h1>
<h3 class="description">View your historical data with customizable time ranges</h3>
</div>
<div>
<h2 class="update">Latest Update: {{ latestUpdate }}</h2>
</div>
<div>
<button (click)="updateInterval('HOURLY')" [class.active-button]="selectedButton === 'hourly'">Hourly</button>
<button (click)="updateInterval('DAILY')" [class.active-button]="selectedButton === 'daily'">Daily</button>
</div>
<div class="graph">
<app-graph [interval]="selectedInterval"></app-graph>
</div>
</div>

View File

@ -0,0 +1,137 @@
.container {
font-family: "Onest", sans-serif;
}
.title {
color: #49473C;
font-size: 30px;
margin-top: 10px;
}
.description {
color: #49473C;
font-size: 15px;
margin-top: 10px;
}
.update{
color: #49473C;
font-size: 15px;
margin-top: 18px;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-top: 20px;
justify-content: flex-start;
}
.card-parameter{
border: 1px solid #16423C;
color: #16423C;
padding: 20px 0px 20px 0px;
border-radius: 8px;
text-align: center;
flex: 1 1 30%;
max-width: 30%;
min-width: 200px;
}
.card-parameter:hover{
background-color: #16423C;
color: white;
}
.card-content{
text-align: center;
margin:auto;
}
.title-graph{
color: #49473C;
font-size: 23px;
font-weight: 400;
margin-top: 45px;
}
.graph{
margin-top: 22px;
}
.relay-container {
padding: 20px 0px 20px 0px;
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: flex-start;
}
.relay-card {
border-radius: 8px;
flex: 1 1 30%;
max-width: 30%;
min-width: 200px;
}
@media (max-width: 768px) {
.card-parameter{
flex: 1 1 45%;
}
.card-container{
justify-content: center;
}
}
@media (max-width: 576px) {
.card-parameter{
flex: 1 1 100%;
}
.card-container{
justify-content: center;
}
}
button {
border: none;
border-radius: 10px;
padding: 5px 10px;
font-size: 15px;
cursor: pointer;
margin-right: 20px;
}
button {
border: none;
border-radius: 10px;
padding: 5px 10px;
font-size: 15px;
cursor: pointer;
margin-right: 20px;
}
.active-button {
background-color: #cad849;
color: white;
}
.loading{
font-size: 18px;
text-align: center;
}
.status-on {
color: #16423C;
}
.status-off {
color: rgb(144, 6, 6);
}
.spinner {
color: #16423C
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistorygraphComponent } from './historygraph.component';
describe('HistorygraphComponent', () => {
let component: HistorygraphComponent;
let fixture: ComponentFixture<HistorygraphComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HistorygraphComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HistorygraphComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { GraphComponent } from '../graph/graph.component';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-historygraph',
standalone: true,
imports: [GraphComponent, CommonModule],
templateUrl: './historygraph.component.html',
styleUrls: ['./historygraph.component.scss']
})
export class HistorygraphComponent implements OnInit, OnDestroy {
selectedButton: string = '';
selectedInterval: string = '';
latestUpdate: string = '';
intervalId: any;
greeting: string = '';
constructor(private cdr: ChangeDetectorRef) {}
@ViewChild(GraphComponent) graphComponent!: GraphComponent;
ngOnInit(): void {
this.startClock();
this.updateGreeting();
this.selectedButton = 'hourly';
this.selectedInterval = 'HOURLY';
}
private debounceTimeout: any;
updateInterval(interval: string): void {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
if (this.selectedInterval !== interval) {
this.selectedInterval = interval;
this.selectedButton = interval.toLowerCase();
this.cdr.detectChanges();
this.graphComponent.updateCharts();
}
}, 300);
}
ngOnDestroy(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
startClock(): void {
this.updateLatestTime();
this.intervalId = setInterval(() => this.updateLatestTime(), 1000);
}
updateLatestTime(): void {
const now = new Date();
this.latestUpdate = now.toLocaleString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
updateGreeting(): void {
const hour = new Date().getHours();
if (hour < 12) {
this.greeting = 'Good Morning';
} else if (hour < 18) {
this.greeting = 'Good Afternoon';
} else {
this.greeting = 'Good Evening';
}
}
}