replaced ugly svg graph by chartjs
This commit is contained in:
parent
5ef3f6b0da
commit
aa71616961
85
src/main/angular/package-lock.json
generated
85
src/main/angular/package-lock.json
generated
@ -19,6 +19,8 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@stomp/ng2-stompjs": "^8.0.0",
|
"@stomp/ng2-stompjs": "^8.0.0",
|
||||||
"@stomp/stompjs": "^7.2.1",
|
"@stomp/stompjs": "^7.2.1",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
|
"ng2-charts": "^8.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
@ -423,6 +425,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/cdk": {
|
||||||
|
"version": "20.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz",
|
||||||
|
"integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^8.0.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^20.0.0 || ^21.0.0",
|
||||||
|
"@angular/core": "^20.0.0 || ^21.0.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "20.3.7",
|
"version": "20.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.7.tgz",
|
||||||
@ -1901,6 +1919,13 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@listr2/prompt-adapter-inquirer": {
|
"node_modules/@listr2/prompt-adapter-inquirer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz",
|
||||||
@ -4198,6 +4223,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-adapter-date-fns": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=2.8.0",
|
||||||
|
"date-fns": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@ -4561,6 +4609,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/date-format": {
|
"node_modules/date-format": {
|
||||||
"version": "4.0.14",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
|
||||||
@ -6767,6 +6826,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/log-symbols": {
|
"node_modules/log-symbols": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||||
@ -7366,6 +7431,24 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ng2-charts": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-nofsNHI2Zt+EAwT+BJBVg0kgOhNo9ukO4CxULlaIi7VwZSr7I1km38kWSoU41Oq6os6qqIh5srnL+CcV+RFPFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash-es": "^4.17.15",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/cdk": ">=19.0.0",
|
||||||
|
"@angular/common": ">=19.0.0",
|
||||||
|
"@angular/core": ">=19.0.0",
|
||||||
|
"@angular/platform-browser": ">=19.0.0",
|
||||||
|
"chart.js": "^3.4.0 || ^4.0.0",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||||
@ -7904,7 +7987,6 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^6.0.0"
|
"entities": "^6.0.0"
|
||||||
@ -7958,7 +8040,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
|
|||||||
@ -35,7 +35,9 @@
|
|||||||
"@stomp/stompjs": "^7.2.1",
|
"@stomp/stompjs": "^7.2.1",
|
||||||
"@fortawesome/angular-fontawesome": "^3.0.0",
|
"@fortawesome/angular-fontawesome": "^3.0.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0"
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
|
"ng2-charts": "^8.0.0",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.3.7",
|
"@angular/build": "^20.3.7",
|
||||||
|
|||||||
@ -9,9 +9,13 @@ import {registerLocaleData} from '@angular/common';
|
|||||||
import localeDe from '@angular/common/locales/de';
|
import localeDe from '@angular/common/locales/de';
|
||||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||||
import {stompServiceFactory} from './common';
|
import {stompServiceFactory} from './common';
|
||||||
|
import {Chart, registerables} from 'chart.js';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||||
|
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
|
|||||||
@ -47,6 +47,16 @@ export function validateEnum<T extends Record<string, string>>(value: any, enumT
|
|||||||
throw new Error(`Invalid enum value: ${str}`);
|
throw new Error(`Invalid enum value: ${str}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function maxDate(a: Date | null | undefined, b: Date | null | undefined): Date | null {
|
||||||
|
if (!a || !b) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (a.getTime() < b.getTime()) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
export function or<T, R, E>(t: T | null | undefined, map: (t: T) => R, orElse: E): R | E {
|
export function or<T, R, E>(t: T | null | undefined, map: (t: T) => R, orElse: E): R | E {
|
||||||
return t === null || t === undefined ? orElse : map(t);
|
return t === null || t === undefined ? orElse : map(t);
|
||||||
}
|
}
|
||||||
@ -218,6 +228,10 @@ export abstract class CrudService<T> {
|
|||||||
this.api.postSingle<T>([...this.path, ...path], data, this.fromJson, next);
|
this.api.postSingle<T>([...this.path, ...path], data, this.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected postSingle2<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): void {
|
||||||
|
this.api.postSingle<T>([...this.path, ...path], data, fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
protected postList(path: any[], data: any, next?: Next<T[]>): void {
|
protected postList(path: any[], data: any, next?: Next<T[]>): void {
|
||||||
this.api.postList<T>([...this.path, ...path], data, this.fromJson, next);
|
this.api.postList<T>([...this.path, ...path], data, this.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<canvas baseChart [data]="data" [options]="options"></canvas>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
div {
|
||||||
|
aspect-ratio: 2;
|
||||||
|
}
|
||||||
159
src/main/angular/src/app/location/energy/charts/energy-charts.ts
Normal file
159
src/main/angular/src/app/location/energy/charts/energy-charts.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {Component, Input, ViewChild} from '@angular/core';
|
||||||
|
import {Interval} from '../../../series/Interval';
|
||||||
|
import {PointService} from '../../../point/point-service';
|
||||||
|
import {Location} from '../../Location';
|
||||||
|
import {BaseChartDirective} from 'ng2-charts';
|
||||||
|
import {ChartConfiguration} from 'chart.js';
|
||||||
|
import {de} from 'date-fns/locale';
|
||||||
|
import {PointSeries} from '../../../point/PointSeries';
|
||||||
|
import {formatNumber} from '@angular/common';
|
||||||
|
|
||||||
|
const COLOR_BACK_PURCHASE = "#ffb9b9";
|
||||||
|
const COLOR_BACK_DELIVER = "#ff59ff";
|
||||||
|
const COLOR_BACK_PRODUCE = "#5cbcff";
|
||||||
|
const COLOR_BACK_SELF = "#60ff8c";
|
||||||
|
const COLOR_BACK_CONSUME = "#ffc07a";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-energy-charts',
|
||||||
|
imports: [
|
||||||
|
BaseChartDirective
|
||||||
|
],
|
||||||
|
templateUrl: './energy-charts.html',
|
||||||
|
styleUrl: './energy-charts.less',
|
||||||
|
})
|
||||||
|
class EnergyCharts {
|
||||||
|
|
||||||
|
private _offset!: number | null;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set offset(value: number | null) {
|
||||||
|
this._offset = value;
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewChild(BaseChartDirective)
|
||||||
|
chart?: BaseChartDirective;
|
||||||
|
|
||||||
|
private _interval!: Interval | null;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set interval(value: Interval | null) {
|
||||||
|
this._interval = value;
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _location!: Location | null;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set location(value: Location | null) {
|
||||||
|
this._location = value;
|
||||||
|
this.fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly pointService: PointService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(): void {
|
||||||
|
if (!this._location || !this._interval || this._offset == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const series = [
|
||||||
|
this._location.energyPurchase,
|
||||||
|
this._location.energyDeliver,
|
||||||
|
this._location.energyProduce,
|
||||||
|
];
|
||||||
|
const location = this._location;
|
||||||
|
const interval = this._interval;
|
||||||
|
const offset = this._offset;
|
||||||
|
this.pointService.relative(series, interval, offset, 1, interval.inner, result => {
|
||||||
|
const energyPurchase = result.series.filter(s => s.series.id === location.energyPurchase?.id)[0] || null;
|
||||||
|
const energyDeliver = result.series.filter(s => s.series.id === location.energyDeliver?.id)[0] || null;
|
||||||
|
const energyProduce = result.series.filter(s => s.series.id === location.energyProduce?.id)[0] || null;
|
||||||
|
const energySelf = energyProduce?.merge(energyDeliver, "Energie Selbst", (a, b) => a - b) || null;
|
||||||
|
this.data.datasets.length = 0;
|
||||||
|
this.add(energyDeliver, COLOR_BACK_DELIVER, -1, "a");
|
||||||
|
this.add(energySelf, COLOR_BACK_SELF, 1, "a");
|
||||||
|
this.add(energyPurchase, COLOR_BACK_PURCHASE, 1, "a");
|
||||||
|
this.chart?.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private add(pointSeries: PointSeries | null, color: string, factor: number, stack: string) {
|
||||||
|
if (!pointSeries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.data.datasets.push({
|
||||||
|
type: 'bar',
|
||||||
|
categoryPercentage: 1.0,
|
||||||
|
barPercentage: 1.0,
|
||||||
|
data: pointSeries.points.map(p => {
|
||||||
|
return {x: p[0] * 1000, y: p[1] * factor};
|
||||||
|
}),
|
||||||
|
label: `${pointSeries.series.name} [${pointSeries.series.unit}]`,
|
||||||
|
borderColor: color,
|
||||||
|
backgroundColor: color,
|
||||||
|
stack: stack ? stack : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected data: ChartConfiguration['data'] = {
|
||||||
|
datasets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
protected options: ChartConfiguration['options'] = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
displayFormats: {
|
||||||
|
minute: "HH:mm",
|
||||||
|
hour: "HH:mm",
|
||||||
|
day: "dd.MM"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: de
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
suggestedMax: 0.5,
|
||||||
|
suggestedMin: -0.1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
itemSort: (a, b) => b.datasetIndex - a.datasetIndex,
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => {
|
||||||
|
const groups = /^Energie (?<name>.+)\[(?<unit>.+)]$/.exec(ctx.dataset.label || '')?.groups;
|
||||||
|
if (groups) {
|
||||||
|
return `${groups['name']}: ${ctx.parsed.y === null ? '-' : formatNumber(ctx.parsed.y, 'de-DE', '0.2-2')} ${groups['unit']}`;
|
||||||
|
}
|
||||||
|
return ctx.label;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnergyCharts
|
||||||
@ -78,6 +78,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-energy-plot [location]="location" [interval]="interval" [offset]="offset"></app-energy-plot>
|
<app-energy-charts [location]="location" [interval]="interval" [offset]="offset"></app-energy-charts>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
import {AfterViewInit, Component, Input, OnDestroy, OnInit, signal} from '@angular/core';
|
||||||
import {Location} from '../Location';
|
import {Location} from '../Location';
|
||||||
import {Series} from '../../series/Series';
|
import {Series} from '../../series/Series';
|
||||||
import {Next} from '../../common';
|
import {Next} from '../../common';
|
||||||
@ -7,18 +7,21 @@ import {PointService} from '../../point/point-service';
|
|||||||
import {SeriesService} from '../../series/series-service';
|
import {SeriesService} from '../../series/series-service';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {Value} from '../../series/Value';
|
import {Value} from '../../series/Value';
|
||||||
import {EnergyPlot} from './plot/energy-plot';
|
import EnergyCharts from './charts/energy-charts';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-location-energy',
|
selector: 'app-location-energy',
|
||||||
imports: [
|
imports: [
|
||||||
EnergyPlot
|
EnergyCharts,
|
||||||
|
EnergyCharts
|
||||||
],
|
],
|
||||||
templateUrl: './location-energy.html',
|
templateUrl: './location-energy.html',
|
||||||
styleUrl: './location-energy.less',
|
styleUrl: './location-energy.less',
|
||||||
})
|
})
|
||||||
export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
export class LocationEnergy implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly signal = signal;
|
||||||
|
|
||||||
protected readonly Interval = Interval;
|
protected readonly Interval = Interval;
|
||||||
|
|
||||||
private readonly subs: Subscription[] = [];
|
private readonly subs: Subscription[] = [];
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
export class EnergyPoint {
|
|
||||||
|
|
||||||
readonly epochSeconds: number;
|
|
||||||
|
|
||||||
readonly date: Date;
|
|
||||||
|
|
||||||
private _purchase: number | null = null;
|
|
||||||
|
|
||||||
private _produce: number | null = null;
|
|
||||||
|
|
||||||
private _deliver: number | null = null;
|
|
||||||
|
|
||||||
private _self: number | null = null;
|
|
||||||
|
|
||||||
private _consume: number | null = null;
|
|
||||||
|
|
||||||
private _purchaseY: number | null = null;
|
|
||||||
|
|
||||||
private _selfY: number | null = null;
|
|
||||||
|
|
||||||
private _deliverY: number | null = null;
|
|
||||||
|
|
||||||
getPurchaseY(yFactor: number): number {
|
|
||||||
return this.getPurchaseH(yFactor) + this.getSelfH(yFactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPurchaseH(yFactor: number): number {
|
|
||||||
if (this._purchaseY === null) {
|
|
||||||
this._purchaseY = (this.purchase || 0) * yFactor;
|
|
||||||
}
|
|
||||||
return this._purchaseY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelfY(yFactor: number): number {
|
|
||||||
return this.getSelfH(yFactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelfH(yFactor: number): number {
|
|
||||||
if (this._selfY === null) {
|
|
||||||
this._selfY = (this.self || 0) * yFactor;
|
|
||||||
}
|
|
||||||
return this._selfY;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDeliverH(yFactor: number): number {
|
|
||||||
if (this._deliverY === null) {
|
|
||||||
this._deliverY = (this.deliver || 0) * yFactor;
|
|
||||||
}
|
|
||||||
return this._deliverY;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
point: number[],
|
|
||||||
) {
|
|
||||||
this.epochSeconds = point[0];
|
|
||||||
this.date = new Date(this.epochSeconds * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
set deliver(value: number | null) {
|
|
||||||
this._deliver = value;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
set produce(value: number | null) {
|
|
||||||
this._produce = value;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
set purchase(value: number | null) {
|
|
||||||
this._purchase = value;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
|
||||||
if (this._purchase !== null && this._produce !== null && this._deliver !== null) {
|
|
||||||
this._self = Math.max(0, this._produce - this._deliver);
|
|
||||||
this._consume = Math.max(0, this._purchase + this._self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get consume(): number | null {
|
|
||||||
return this._consume;
|
|
||||||
}
|
|
||||||
|
|
||||||
get self(): number | null {
|
|
||||||
return this._self;
|
|
||||||
}
|
|
||||||
|
|
||||||
get deliver(): number | null {
|
|
||||||
return this._deliver;
|
|
||||||
}
|
|
||||||
|
|
||||||
get produce(): number | null {
|
|
||||||
return this._produce;
|
|
||||||
}
|
|
||||||
|
|
||||||
get purchase(): number | null {
|
|
||||||
return this._purchase;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<svg [attr.viewBox]="`0 0 ${widthPx} ${heightPx+1}`" [style.background-color]="'#eee'">
|
|
||||||
@for (point of points; track point.epochSeconds) {
|
|
||||||
<g>
|
|
||||||
<title>
|
|
||||||
Bezug: {{ location.energyPurchase?.toValue(point.purchase, point.date)?.toValueString(null) }}
|
|
||||||
Solar: {{ location.energyProduce?.toValue(point.produce, point.date)?.toValueString(null) }}
|
|
||||||
Selbst: {{ location.energyPurchase?.toValue(point.self, point.date)?.toValueString(null) }}
|
|
||||||
Einsp.: {{ location.energyDeliver?.toValue(point.deliver, point.date)?.toValueString(null) }}
|
|
||||||
Verbrauch: {{ location.energyPurchase?.toValue(point.consume, point.date)?.toValueString(null) }}
|
|
||||||
</title>
|
|
||||||
<rect
|
|
||||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
|
||||||
[attr.y]="heightPx + yMinPx - point.getPurchaseY(yFactor)"
|
|
||||||
[attr.width]="xWidthPx"
|
|
||||||
[attr.height]="point.getPurchaseH(yFactor)"
|
|
||||||
class="COLOR_BACK_PURCHASE"
|
|
||||||
></rect>
|
|
||||||
<rect
|
|
||||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
|
||||||
[attr.y]="heightPx + yMinPx - point.getSelfY(yFactor)"
|
|
||||||
[attr.width]="xWidthPx"
|
|
||||||
[attr.height]="point.getSelfH(yFactor)"
|
|
||||||
class="COLOR_BACK_SELF"
|
|
||||||
></rect>
|
|
||||||
<rect
|
|
||||||
[attr.x]="(point.epochSeconds - xMin) * xFactor"
|
|
||||||
[attr.y]="heightPx+1 + yMinPx"
|
|
||||||
[attr.width]="xWidthPx"
|
|
||||||
[attr.height]="point.getDeliverH(yFactor)"
|
|
||||||
class="COLOR_BACK_DELIVER"
|
|
||||||
></rect>
|
|
||||||
</g>
|
|
||||||
}
|
|
||||||
<line
|
|
||||||
x1="0"
|
|
||||||
[attr.y1]="heightPx +0.5 + yMinPx"
|
|
||||||
[attr.x2]="widthPx"
|
|
||||||
[attr.y2]="heightPx +0.5 + yMinPx"
|
|
||||||
stroke="#aaaaaa"
|
|
||||||
stroke-width="1"
|
|
||||||
></line>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,5 +0,0 @@
|
|||||||
@import "../../../../colors";
|
|
||||||
|
|
||||||
g:hover {
|
|
||||||
stroke: black;
|
|
||||||
}
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
|
||||||
import {Location} from '../../Location';
|
|
||||||
import {Interval} from '../../../series/Interval';
|
|
||||||
import {PointService} from '../../../point/point-service';
|
|
||||||
import {PointResponse} from '../../../point/PointResponse';
|
|
||||||
import {PointSeries} from '../../../point/PointSeries';
|
|
||||||
import {EnergyPoint} from './EnergyPoint';
|
|
||||||
import {Subscription, timer} from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-energy-plot',
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './energy-plot.html',
|
|
||||||
styleUrl: './energy-plot.less',
|
|
||||||
})
|
|
||||||
export class EnergyPlot implements OnInit, OnDestroy, AfterViewInit {
|
|
||||||
|
|
||||||
readonly widthPx = 800;
|
|
||||||
|
|
||||||
readonly heightPx = 100;
|
|
||||||
|
|
||||||
private _location!: Location;
|
|
||||||
|
|
||||||
private _interval: Interval = Interval.FIVE;
|
|
||||||
|
|
||||||
private _offset: number = 0;
|
|
||||||
|
|
||||||
private _count: number = 1;
|
|
||||||
|
|
||||||
protected points: EnergyPoint[] = [];
|
|
||||||
|
|
||||||
protected yMin: number = 0;
|
|
||||||
|
|
||||||
protected yMinPx: number = 0;
|
|
||||||
|
|
||||||
protected yMax: number = 0;
|
|
||||||
|
|
||||||
protected yFactor: number = 0;
|
|
||||||
|
|
||||||
protected xMin: number = 0;
|
|
||||||
|
|
||||||
protected xMax: number = 0;
|
|
||||||
|
|
||||||
protected xFactor: number = 0;
|
|
||||||
|
|
||||||
protected xWidthPx: number = 0;
|
|
||||||
|
|
||||||
private readonly subs: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly pointService: PointService,
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.subs.push(timer(60000, 60000).subscribe(() => this.ngAfterViewInit()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subs.forEach(sub => sub.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
if (!this._location.energyPurchase) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this._location.energyProduce) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this._location.energyDeliver) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const series = [this._location.energyPurchase, this._location.energyProduce, this._location.energyDeliver];
|
|
||||||
this.pointService.relative(series, this._interval, this._offset, this._count, this._interval.inner, this.update);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly update = (response: PointResponse): void => {
|
|
||||||
this.points.length = 0;
|
|
||||||
this.add(response.series[0], (p, v) => p.purchase = v);
|
|
||||||
this.add(response.series[1], (p, v) => p.produce = v);
|
|
||||||
this.add(response.series[2], (p, v) => p.deliver = v);
|
|
||||||
this.yMax = -Infinity;
|
|
||||||
this.yMin = Infinity;
|
|
||||||
for (let point of this.points) {
|
|
||||||
this.yMax = Math.max(this.yMax, point.consume || 0);
|
|
||||||
this.yMin = Math.min(this.yMin, -(point.deliver || 0));
|
|
||||||
}
|
|
||||||
this.yMinPx = this.yMin * this.yFactor;
|
|
||||||
this.yFactor = this.heightPx / (this.yMax - this.yMin);
|
|
||||||
this.xMin = response.begin.getTime() / 1000;
|
|
||||||
this.xMax = response.end.getTime() / 1000;
|
|
||||||
this.xFactor = this.widthPx / (this.xMax - this.xMin);
|
|
||||||
this.xWidthPx = this.widthPx / response.expectedCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
private add(series: PointSeries, setter: (p: EnergyPoint, v: number) => void): void {
|
|
||||||
for (const point of series.points) {
|
|
||||||
const index = this.insert(point, setter);
|
|
||||||
if (index >= 0) {
|
|
||||||
const fresh = new EnergyPoint(point);
|
|
||||||
setter(fresh, point[1])
|
|
||||||
this.points.splice(index, 0, fresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private insert(point: number[], setter: (p: EnergyPoint, v: number) => any): number {
|
|
||||||
let index = 0;
|
|
||||||
for (let old of this.points) {
|
|
||||||
const age = old.epochSeconds - point[0];
|
|
||||||
if (age === 0) {
|
|
||||||
setter(old, point[1])
|
|
||||||
return -1;
|
|
||||||
} else if (age < 0) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set location(value: Location) {
|
|
||||||
this._location = value;
|
|
||||||
this.ngAfterViewInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
get location(): Location {
|
|
||||||
return this._location;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set interval(value: Interval) {
|
|
||||||
this._interval = value;
|
|
||||||
this.ngAfterViewInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set offset(value: number) {
|
|
||||||
this._offset = value;
|
|
||||||
this.ngAfterViewInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set count(value: number) {
|
|
||||||
this._count = value;
|
|
||||||
this.ngAfterViewInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {Series} from "../series/Series";
|
import {Series} from "../series/Series";
|
||||||
import {validateList, validateNumber} from "../common";
|
import {maxDate, validateList, validateNumber} from "../common";
|
||||||
|
|
||||||
export class PointSeries {
|
export class PointSeries {
|
||||||
|
|
||||||
@ -17,4 +17,41 @@ export class PointSeries {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
merge(that: PointSeries | null, name: string, fun: (a: number, b: number) => number): PointSeries | null {
|
||||||
|
if (!that) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.series.type !== that.series.type) {
|
||||||
|
throw new Error(`Cannot combine PointSeries of different Series.type: this=${this.series.name}/${this.series.type}, that=${that.series.name}/${that.series.type}`);
|
||||||
|
}
|
||||||
|
const a = [...this.points];
|
||||||
|
const b = [...that.points];
|
||||||
|
let ai = 0;
|
||||||
|
let bi = 0;
|
||||||
|
const result: number[][] = [];
|
||||||
|
while (ai < a.length && ai < b.length) {
|
||||||
|
const av = a[ai];
|
||||||
|
const bv = b[bi];
|
||||||
|
const at = av[0];
|
||||||
|
const bt = bv[0];
|
||||||
|
if (at < bt) {
|
||||||
|
ai++;
|
||||||
|
} else if (bt < at) {
|
||||||
|
bi++;
|
||||||
|
} else {
|
||||||
|
const r = [at];
|
||||||
|
for (let i = 1; i < bv.length; i++) {
|
||||||
|
r.push(fun(av[i], bv[i]));
|
||||||
|
}
|
||||||
|
result.push(r)
|
||||||
|
ai++;
|
||||||
|
bi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const value = fun(this.series.value.value, that.series.value.value);
|
||||||
|
const last = maxDate(this.series.last, that.series.last);
|
||||||
|
const series = new Series(-1, name, this.series.precision, this.series.seconds, this.series.type, value, this.series.unit, last);
|
||||||
|
return new PointSeries(series, result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import {PointResponse} from './PointResponse';
|
|||||||
import {Series} from '../series/Series';
|
import {Series} from '../series/Series';
|
||||||
import {Interval} from '../series/Interval';
|
import {Interval} from '../series/Interval';
|
||||||
|
|
||||||
|
const notNull = <T>(v: T | null | undefined): v is T => v != null;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@ -16,9 +18,9 @@ export class PointService extends CrudService<PointResponse> {
|
|||||||
super(api, ws, ['Point'], PointResponse.fromJson);
|
super(api, ws, ['Point'], PointResponse.fromJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
relative(series: Series[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
|
relative(series: (Series | null | undefined)[], outer: Interval, offset: number, count: number, interval: Interval, next: Next<PointResponse>): void {
|
||||||
const request = {
|
const request = {
|
||||||
ids: series.map(s => s.id),
|
ids: series.filter(notNull).map(s => s.id),
|
||||||
outerInterval: outer.name,
|
outerInterval: outer.name,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
count: count,
|
count: count,
|
||||||
|
|||||||
@ -29,6 +29,9 @@ export class Value {
|
|||||||
return `--- ${this.unit}`
|
return `--- ${this.unit}`
|
||||||
}
|
}
|
||||||
const scale = Math.floor(Math.log10(this.value));
|
const scale = Math.floor(Math.log10(this.value));
|
||||||
|
if(isNaN(scale)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
const rest = scale - this.precision + 1;
|
const rest = scale - this.precision + 1;
|
||||||
if (rest >= 0) {
|
if (rest >= 0) {
|
||||||
return `${Math.round(this.value)} ${this.unit}`;
|
return `${Math.round(this.value)} ${this.unit}`;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.ph87.data.point;
|
package de.ph87.data.point;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -15,6 +16,7 @@ public class PointController {
|
|||||||
|
|
||||||
private final PointService pointService;
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@PostMapping("relative")
|
@PostMapping("relative")
|
||||||
public PointResponse points(@RequestBody final PointRequestRelative request) {
|
public PointResponse points(@RequestBody final PointRequestRelative request) {
|
||||||
return pointService.points(request);
|
return pointService.points(request);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user