Creation dashboard (graph chart.js) + tests
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<canvas #canvas></canvas>
|
||||
@@ -0,0 +1,6 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { ConsumptionGauge } from './consumption-gauge';
|
||||
|
||||
vi.mock('chart.js', () => {
|
||||
class ChartMock {
|
||||
update = vi.fn();
|
||||
data = { datasets: [{}] };
|
||||
static register = vi.fn();
|
||||
}
|
||||
return { Chart: ChartMock, registerables: [] };
|
||||
});
|
||||
|
||||
describe('ConsumptionGauge', () => {
|
||||
it('se crée sans erreur avec des entrées valides', () => {
|
||||
TestBed.configureTestingModule({ imports: [ConsumptionGauge] });
|
||||
const fixture = TestBed.createComponent(ConsumptionGauge);
|
||||
fixture.componentRef.setInput('consumption', 300);
|
||||
fixture.componentRef.setInput('capacity', 1000);
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
});
|
||||
it('met à jour le graphique quand les valeurs changent après initialisation', () => {
|
||||
TestBed.configureTestingModule({ imports: [ConsumptionGauge] });
|
||||
const fixture = TestBed.createComponent(ConsumptionGauge);
|
||||
fixture.componentRef.setInput('consumption', 300);
|
||||
fixture.componentRef.setInput('capacity', 1000);
|
||||
fixture.detectChanges(); // déclenche ngAfterViewInit, this.chart existe désormais
|
||||
|
||||
fixture.componentRef.setInput('consumption', 500);
|
||||
fixture.detectChanges(); // ré-exécute l'effect, cette fois avec this.chart défini
|
||||
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component, ElementRef, ViewChild, input, effect, AfterViewInit } from '@angular/core';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
@Component({
|
||||
selector: 'app-consumption-gauge',
|
||||
standalone: true,
|
||||
templateUrl: './consumption-gauge.html',
|
||||
styleUrl: './consumption-gauge.scss',
|
||||
})
|
||||
export class ConsumptionGauge implements AfterViewInit {
|
||||
consumption = input.required<number>();
|
||||
capacity = input.required<number>();
|
||||
|
||||
@ViewChild('canvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private chart?: Chart;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const used = this.consumption();
|
||||
const remaining = Math.max(0, this.capacity() - used);
|
||||
if (this.chart) {
|
||||
this.chart.data.datasets[0].data = [used, remaining];
|
||||
this.chart.update('none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const used = this.consumption();
|
||||
const remaining = Math.max(0, this.capacity() - used);
|
||||
|
||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Utilisé', 'Disponible'],
|
||||
datasets: [
|
||||
{
|
||||
data: [used, remaining],
|
||||
backgroundColor: ['#3b82f6', '#e5e7eb'],
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '70%',
|
||||
animation: { duration: 300 },
|
||||
plugins: { legend: { display: false } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<canvas #canvas></canvas>
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 260px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { SiteLoadChart } from './site-load-chart';
|
||||
|
||||
vi.mock('chart.js', () => {
|
||||
class ChartMock {
|
||||
update = vi.fn();
|
||||
data = { datasets: [{}] };
|
||||
static register = vi.fn();
|
||||
}
|
||||
return { Chart: ChartMock, registerables: [] };
|
||||
});
|
||||
|
||||
describe('SiteLoadChart', () => {
|
||||
it('se crée sans erreur avec une liste de sites valide', () => {
|
||||
TestBed.configureTestingModule({ imports: [SiteLoadChart] });
|
||||
const fixture = TestBed.createComponent(SiteLoadChart);
|
||||
fixture.componentRef.setInput('sites', [
|
||||
{ site_id: 'S1', site_name: 'Test', current_consumption_kw: 50, capacity_kw: 100, load_percent: 50, data_quality: 'good' },
|
||||
]);
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
});
|
||||
it('met à jour le graphique quand les sites changent après initialisation', () => {
|
||||
TestBed.configureTestingModule({ imports: [SiteLoadChart] });
|
||||
const fixture = TestBed.createComponent(SiteLoadChart);
|
||||
fixture.componentRef.setInput('sites', [
|
||||
{ site_id: 'S1', site_name: 'A', current_consumption_kw: 50, capacity_kw: 100, load_percent: 50, data_quality: 'good' },
|
||||
]);
|
||||
fixture.detectChanges(); // déclenche ngAfterViewInit, this.chart existe désormais
|
||||
|
||||
fixture.componentRef.setInput('sites', [
|
||||
{ site_id: 'S2', site_name: 'B', current_consumption_kw: 80, capacity_kw: 100, load_percent: 80, data_quality: 'critical' },
|
||||
]);
|
||||
fixture.detectChanges(); // ré-exécute l'effect, cette fois avec this.chart défini
|
||||
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Component, ElementRef, ViewChild, input, effect, AfterViewInit } from '@angular/core';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { SiteSummary } from '../../models/stats.model';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
const QUALITY_COLORS: Record<SiteSummary['data_quality'], string> = {
|
||||
good: '#2e7d32',
|
||||
partial: '#f9a825',
|
||||
degraded: '#ef6c00',
|
||||
critical: '#c62828',
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-site-load-chart',
|
||||
standalone: true,
|
||||
templateUrl: './site-load-chart.html',
|
||||
styleUrl: './site-load-chart.scss',
|
||||
})
|
||||
export class SiteLoadChart implements AfterViewInit {
|
||||
sites = input.required<SiteSummary[]>();
|
||||
|
||||
@ViewChild('canvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private chart?: Chart;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const sites = this.sites();
|
||||
if (this.chart) {
|
||||
this.chart.data.labels = sites.map((s) => s.site_name);
|
||||
this.chart.data.datasets[0].data = sites.map((s) => s.load_percent ?? 0);
|
||||
this.chart.data.datasets[0].backgroundColor = sites.map((s) => QUALITY_COLORS[s.data_quality]);
|
||||
this.chart.update('none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const sites = this.sites();
|
||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: sites.map((s) => s.site_name),
|
||||
datasets: [
|
||||
{
|
||||
data: sites.map((s) => s.load_percent ?? 0),
|
||||
backgroundColor: sites.map((s) => QUALITY_COLORS[s.data_quality]),
|
||||
borderRadius: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, max: 100, title: { display: true, text: 'Charge (%)' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user