diff --git a/src/app/app.component.css b/src/app/app.component.css index e69de29..77fa3f9 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -0,0 +1,84 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: Arial, sans-serif; +} + +header { + margin-bottom: 20px; + text-align: center; +} + +h1 { + color: #333; +} + +.controls { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; +} + +.form-group { + margin-bottom: 10px; + flex: 1; + min-width: 200px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +select, input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.chart-container { + background-color: #f9f9f9; + border-radius: 8px; + padding: 20px; + min-height: 400px; + margin-bottom: 20px; +} + +.loading, .error { + display: flex; + justify-content: center; + align-items: center; + height: 400px; +} + +.error { + color: #d9534f; +} + +.price-list { + margin-top: 30px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 10px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f2f2f2; +} + +tr:hover { + background-color: #f5f5f5; +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 36093e1..2c16ce5 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,336 +1,69 @@ - - - - - - - - +
+
+

{{ title }}

+
- - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - +
+
+ + +
+ +
+ +
-
- - - - - - - +
+ @if (loading) { +
+

Loading energy price data...

+
+ } + @if (error) { +
+

{{ error }}

+
+ } - + @if (!loading && !error) { + + } +
+ + @if (!loading && !error && priceData.length > 0) { +
+

Hour-by-hour prices

+ + + + + + + + + + @for (price of priceData; track price.time_start) { + + + + + + } + +
TimeSEK/kWhEUR/kWh
{{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }}{{ price.SEK_per_kWh | number:'1.2-4' }}{{ price.EUR_per_kWh | number:'1.2-4' }}
+
+ } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts deleted file mode 100644 index 5558c4f..0000000 --- a/src/app/app.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have the 'Angular-DotIO' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('Angular-DotIO'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Angular-DotIO'); - }); -}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fbb0c2d..8e98ba0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,12 +1,70 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { EnergyPriceService, EnergyPrice } from './energy-price.service'; +import { EnergyChartComponent } from './energy-chart/energy-chart.component'; @Component({ selector: 'app-root', - imports: [RouterOutlet], templateUrl: './app.component.html', - styleUrl: './app.component.css' + styleUrls: ['./app.component.css'], + standalone: true, + imports: [CommonModule, FormsModule, EnergyChartComponent] }) -export class AppComponent { - title = 'Angular-DotIO'; +export class AppComponent implements OnInit { + title = 'Energy Price Dashboard'; + priceData: EnergyPrice[] = []; + loading = true; + error = ''; + + // Default values + selectedDate = new Date(); + selectedRegion = 'SE3'; // Stockholm / Södra Mellansverige as default + + regions = [ + { value: 'SE1', label: 'Luleå / Norra Sverige' }, + { value: 'SE2', label: 'Sundsvall / Norra Mellansverige' }, + { value: 'SE3', label: 'Stockholm / Södra Mellansverige' }, + { value: 'SE4', label: 'Malmö / Södra Sverige' } + ]; + + private energyPriceService = inject(EnergyPriceService); + + ngOnInit() { + this.loadPriceData(); + } + + loadPriceData() { + this.loading = true; + this.error = ''; + + const { year, month, day } = this.energyPriceService.formatDate(this.selectedDate); + + this.energyPriceService.getPrices(year, month, day, this.selectedRegion) + .subscribe({ + next: (data) => { + this.priceData = data; + this.loading = false; + }, + error: (err) => { + this.error = 'Failed to load energy price data. Please try again later.'; + this.loading = false; + console.error('Error fetching price data:', err); + } + }); + } + + onRegionChange() { + this.loadPriceData(); + } + + onDateChange(event: any) { + this.selectedDate = new Date(event.target.value); + this.loadPriceData(); + } + + get maxDate(): string { + const today = new Date(); + return today.toISOString().split('T')[0]; + } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a1e7d6f..6896639 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; +import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)] + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideCharts(withDefaultRegisterables())] }; diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..afe33a2 --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; +import { NgChartsModule } from 'ng2-charts'; + +import { AppComponent } from './app.component'; +import { EnergyPriceService } from './energy-price.service'; +import { EnergyChartComponent } from './energy-chart/energy-chart.component'; + +@NgModule({ + declarations: [ + AppComponent, + EnergyChartComponent + ], + imports: [ + BrowserModule, + HttpClientModule, + FormsModule, + NgChartsModule + ], + providers: [EnergyPriceService], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/src/app/energy-chart/energy-chart.component.css b/src/app/energy-chart/energy-chart.component.css new file mode 100644 index 0000000..cf8b246 --- /dev/null +++ b/src/app/energy-chart/energy-chart.component.css @@ -0,0 +1,4 @@ +.chart-wrapper { + height: 400px; + width: 100%; +} diff --git a/src/app/energy-chart/energy-chart.component.html b/src/app/energy-chart/energy-chart.component.html new file mode 100644 index 0000000..f0665d3 --- /dev/null +++ b/src/app/energy-chart/energy-chart.component.html @@ -0,0 +1,8 @@ +
+ + +
+ diff --git a/src/app/energy-chart/energy-chart.component.ts b/src/app/energy-chart/energy-chart.component.ts new file mode 100644 index 0000000..dc9849e --- /dev/null +++ b/src/app/energy-chart/energy-chart.component.ts @@ -0,0 +1,105 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ChartConfiguration, ChartData, ChartType } from 'chart.js'; +import { BaseChartDirective } from 'ng2-charts'; +import { EnergyPrice } from '../energy-price.service'; + +@Component({ + selector: 'app-energy-chart', + templateUrl: './energy-chart.component.html', + styleUrls: ['./energy-chart.component.css'], + standalone: true, + imports: [CommonModule, BaseChartDirective] +}) +export class EnergyChartComponent implements OnChanges { + @Input() priceData: EnergyPrice[] = []; + + // Chart configuration + public lineChartType: ChartType = 'line'; + + public lineChartData: ChartData<'line'> = { + datasets: [], + labels: [] + }; + + public lineChartOptions: ChartConfiguration['options'] = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + title: { + display: true, + text: 'Time' + } + }, + y: { + title: { + display: true, + text: 'Price (SEK per kWh)' + }, + beginAtZero: true + } + }, + plugins: { + legend: { + display: true, + }, + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += context.parsed.y.toFixed(2) + ' SEK/kWh'; + } + return label; + } + } + } + } + }; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['priceData'] && this.priceData) { + this.updateChartData(); + } + } + + private updateChartData(): void { + // Extract time labels and price values + const labels = this.priceData.map(item => { + const startTime = new Date(item.time_start); + return startTime.getHours() + ':00'; + }); + + const sekPrices = this.priceData.map(item => item.SEK_per_kWh); + const eurPrices = this.priceData.map(item => item.EUR_per_kWh); + + // Update chart data + this.lineChartData = { + labels: labels, + datasets: [ + { + data: sekPrices, + label: 'SEK per kWh', + backgroundColor: 'rgba(66, 133, 244, 0.2)', + borderColor: 'rgb(66, 133, 244)', + pointBackgroundColor: 'rgb(66, 133, 244)', + fill: 'origin', + tension: 0.4 + }, + { + data: eurPrices, + label: 'EUR per kWh', + backgroundColor: 'rgba(15, 157, 88, 0.2)', + borderColor: 'rgb(15, 157, 88)', + pointBackgroundColor: 'rgb(15, 157, 88)', + fill: 'origin', + tension: 0.4 + } + ] + }; + } +} diff --git a/src/app/energy-price.service.ts b/src/app/energy-price.service.ts new file mode 100644 index 0000000..33440e5 --- /dev/null +++ b/src/app/energy-price.service.ts @@ -0,0 +1,32 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export interface EnergyPrice { + SEK_per_kWh: number; + EUR_per_kWh: number; + EXR: number; + time_start: string; + time_end: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class EnergyPriceService { + private apiBaseUrl = 'https://www.elprisetjustnu.se/api/v1/prices'; + private http = inject(HttpClient); + + getPrices(year: string, month: string, day: string, priceClass: string): Observable { + const url = `${this.apiBaseUrl}/${year}/${month}-${day}_${priceClass}.json`; + return this.http.get(url); + } + + formatDate(date: Date): { year: string, month: string, day: string } { + const year = date.getFullYear().toString(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + + return { year, month, day }; + } +} diff --git a/src/main.ts b/src/main.ts index 35b00f3..4e6be2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,11 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; +import { provideHttpClient } from '@angular/common/http'; +import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { AppComponent } from './app/app.component'; -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, { + providers: [ + provideHttpClient(), + provideCharts(withDefaultRegisterables()) + ] +}).catch(err => console.error(err));