GraphOperation

This commit is contained in:
Patrick Haßel 2025-10-27 08:39:57 +01:00
parent ab7f3de9a2
commit d65628a7d6
25 changed files with 454 additions and 101 deletions

View File

@ -53,6 +53,14 @@ export function validateBoolean(json: any): boolean {
return json; return json;
} }
export function validateEnum<T extends Record<string, string>>(value: any, enumType: T): T[keyof T] {
const str = validateString(value);
if (Object.values(enumType).includes(str)) {
return str as T[keyof T];
}
throw new Error(`Invalid enum value: ${str}`);
}
export function validateList<T>(json: any, fromJson: FromJson<T>): T[] { export function validateList<T>(json: any, fromJson: FromJson<T>): T[] {
return json.map(fromJson); return json.map(fromJson);
} }

View File

@ -1,10 +1,20 @@
import {Series} from "../../../series/Series"; import {Series} from "../../../series/Series";
import {Group} from "../../Group"; import {Group} from "../../Group";
import {validateBoolean, validateNumber, validateString} from "../../../COMMON"; import {mapNotNull, validateBoolean, validateEnum, validateNumber, validateString} from "../../../COMMON";
import {Axis} from '../Axis'; import {Axis} from '../Axis';
import {SeriesType} from '../../../series/SeriesType'; import {SeriesType} from '../../../series/SeriesType';
import {GraphType} from './GraphType'; import {GraphType} from './GraphType';
export enum GraphOperation {
MINUS = 'MINUS',
PLUS = 'PLUS',
DIVIDE = 'DIVIDE',
}
export function listGraphOperation() {
return Object.keys(GraphOperation);
}
export class Graph { export class Graph {
readonly showMin: boolean; readonly showMin: boolean;
@ -18,12 +28,15 @@ export class Graph {
readonly id: number, readonly id: number,
readonly version: number, readonly version: number,
readonly series: Series, readonly series: Series,
readonly factor: number,
readonly operation: GraphOperation,
readonly series2: Series | null,
readonly factor2: number,
readonly name: string, readonly name: string,
readonly visible: boolean, readonly visible: boolean,
readonly type: GraphType, readonly type: GraphType,
readonly fill: string, readonly fill: string,
readonly color: string, readonly color: string,
readonly factor: number,
readonly group: Group, readonly group: Group,
readonly stack: string, readonly stack: string,
readonly min: boolean, readonly min: boolean,
@ -41,12 +54,15 @@ export class Graph {
validateNumber(json.id), validateNumber(json.id),
validateNumber(json.version), validateNumber(json.version),
Series.fromJson(json.series), Series.fromJson(json.series),
validateNumber(json.factor),
validateEnum(json.operation, GraphOperation),
mapNotNull(json.series2, Series.fromJson),
validateNumber(json.factor2),
validateString(json.name), validateString(json.name),
validateBoolean(json.visible), validateBoolean(json.visible),
GraphType.fromJson(json.type), GraphType.fromJson(json.type),
validateString(json.fill), validateString(json.fill),
validateString(json.color), validateString(json.color),
validateNumber(json.factor),
validateString(json.group) as Group, validateString(json.group) as Group,
validateString(json.stack), validateString(json.stack),
validateBoolean(json.min), validateBoolean(json.min),

View File

@ -136,9 +136,9 @@
</th> </th>
<th>Name</th> <th>Name</th>
<th>Serie</th> <th>Serie</th>
<th>Faktor</th>
<th>Typ</th> <th>Typ</th>
<th>Farbe</th> <th>Farbe</th>
<th>Faktor</th>
<th>Aggregat</th> <th>Aggregat</th>
<th>Stack</th> <th>Stack</th>
<th class="vertical">min</th> <th class="vertical">min</th>
@ -163,6 +163,27 @@
} }
</select> </select>
</td> </td>
<td>
<app-number-nn [initial]="graph.factor" (onChange)="plotService.graphFactor(graph, $event)"></app-number-nn>
</td>
<td>
<select [ngModel]="graph.operation" (ngModelChange)="plotService.graphOperation(graph, $event)">
@for (operation of listGraphOperation(); track operation) {
<option [ngValue]="operation">{{ operation }}</option>
}
</select>
</td>
<td>
<select [ngModel]="graph.series2?.id" (ngModelChange)="plotService.graphSeries2(graph, $event)">
<option [ngValue]="null">-</option>
@for (s of seriesService.list; track s.name) {
<option [ngValue]="s.id">{{ s.name }} [{{ s.valueString }}]</option>
}
</select>
</td>
<td>
<app-number-nn [initial]="graph.factor2" (onChange)="plotService.graphFactor2(graph, $event)"></app-number-nn>
</td>
<td> <td>
<select [ngModel]="graph.type" (ngModelChange)="plotService.graphType(graph, $event)"> <select [ngModel]="graph.type" (ngModelChange)="plotService.graphType(graph, $event)">
@for (type of GraphType.values; track type.jsonName) { @for (type of GraphType.values; track type.jsonName) {
@ -173,9 +194,6 @@
<td> <td>
<app-text [initial]="graph.color" (onChange)="plotService.graphColor(graph, $event)"></app-text> <app-text [initial]="graph.color" (onChange)="plotService.graphColor(graph, $event)"></app-text>
</td> </td>
<td>
<app-number-nn [initial]="graph.factor" (onChange)="plotService.graphFactor(graph, $event)"></app-number-nn>
</td>
<td> <td>
<select [ngModel]="graph.group" (ngModelChange)="plotService.graphGroup(graph, $event)"> <select [ngModel]="graph.group" (ngModelChange)="plotService.graphGroup(graph, $event)">
@for (group of groups(); track group) { @for (group of groups(); track group) {

View File

@ -19,6 +19,7 @@ import {Location} from '@angular/common';
import {PlotComponent} from '../plot/plot.component'; import {PlotComponent} from '../plot/plot.component';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {GraphType} from '../axis/graph/GraphType'; import {GraphType} from '../axis/graph/GraphType';
import {GraphOperation, listGraphOperation} from '../axis/graph/Graph';
Chart.register( Chart.register(
CategoryScale, CategoryScale,
@ -35,13 +36,6 @@ Chart.register(
Filler, Filler,
); );
export function unitInBrackets(unit: string): string {
if (!unit) {
return '';
}
return ` [${unit}]`;
}
@Component({ @Component({
selector: 'app-plot-editor', selector: 'app-plot-editor',
imports: [ imports: [
@ -58,6 +52,12 @@ export function unitInBrackets(unit: string): string {
}) })
export class PlotEditor implements OnInit, OnDestroy { export class PlotEditor implements OnInit, OnDestroy {
protected readonly GraphType = GraphType;
protected readonly GraphOperation = GraphOperation;
protected readonly listGraphOperation = listGraphOperation;
protected readonly SeriesType = SeriesType; protected readonly SeriesType = SeriesType;
protected readonly Interval = Interval; protected readonly Interval = Interval;
@ -142,5 +142,4 @@ export class PlotEditor implements OnInit, OnDestroy {
return Object.keys(Group); return Object.keys(Group);
} }
protected readonly GraphType = GraphType;
} }

View File

@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
import {ApiService, EntityListService, Next} from '../COMMON'; import {ApiService, EntityListService, Next} from '../COMMON';
import {Plot} from './Plot'; import {Plot} from './Plot';
import {Axis} from './axis/Axis'; import {Axis} from './axis/Axis';
import {Graph} from './axis/graph/Graph'; import {Graph, GraphOperation} from './axis/graph/Graph';
import {Group} from './Group'; import {Group} from './Group';
import {Interval} from '../series/Interval'; import {Interval} from '../series/Interval';
import {GraphType} from './axis/graph/GraphType'; import {GraphType} from './axis/graph/GraphType';
@ -118,14 +118,26 @@ export class PlotService extends EntityListService<Plot> {
this.postSingle(['Graph', graph.id, 'series'], value, next); this.postSingle(['Graph', graph.id, 'series'], value, next);
} }
graphColor(graph: Graph, value: string, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'color'], value, next);
}
graphFactor(graph: Graph, value: number, next?: Next<Plot>): void { graphFactor(graph: Graph, value: number, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'factor'], value, next); this.postSingle(['Graph', graph.id, 'factor'], value, next);
} }
graphOperation(graph: Graph, value: GraphOperation, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'operation'], value, next);
}
graphSeries2(graph: Graph, value: number, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'series2'], value, next);
}
graphFactor2(graph: Graph, value: number, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'factor2'], value, next);
}
graphColor(graph: Graph, value: string, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'color'], value, next);
}
graphGroup(graph: Graph, value: Group, next?: Next<Plot>): void { graphGroup(graph: Graph, value: Group, next?: Next<Plot>): void {
this.postSingle(['Graph', graph.id, 'group'], value, next); this.postSingle(['Graph', graph.id, 'group'], value, next);
} }

View File

@ -216,24 +216,21 @@ export class PlotComponent implements AfterViewInit, OnDestroy {
private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void { private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void {
this.seriesService.oneSeriesPoints( this.seriesService.oneSeriesPoints(
graph.series, graph,
graph.axis.plot.interval,
graph.axis.plot.offset,
graph.axis.plot.duration,
points => { points => {
if (graph.series.type === SeriesType.BOOL) { if (graph.series.type === SeriesType.BOOL) {
mid.data = toBool(points, graph.factor); mid.data = toBool(points);
} else if (graph.series.type === SeriesType.DELTA) { } else if (graph.series.type === SeriesType.DELTA) {
mid.data = toDelta(points, graph.factor); mid.data = toDelta(points);
} else if (graph.series.type === SeriesType.VARYING) { } else if (graph.series.type === SeriesType.VARYING) {
if (min) { if (min) {
min.data = toMin(points, graph.factor); min.data = toMin(points);
} }
if (max) { if (max) {
max.data = toMax(points, graph.factor); max.data = toMax(points);
} }
if (mid) { if (mid) {
mid.data = toAvg(points, graph.factor); mid.data = toAvg(points);
} }
} }
this.chart.update(); this.chart.update();
@ -264,14 +261,13 @@ export class PlotComponent implements AfterViewInit, OnDestroy {
private updatePoint(graph: Graph, dataset: Dataset, date: Date, y: number): void { private updatePoint(graph: Graph, dataset: Dataset, date: Date, y: number): void {
const x = date.getTime(); const x = date.getTime();
const point = dataset.data.filter((p: PointElement) => p.x === x)[0]; const point = dataset.data.filter((p: PointElement) => p.x === x)[0];
const yMultiplied = y * graph.factor;
if (point) { if (point) {
if (point.y !== yMultiplied) { if (point.y !== y) {
point.y = yMultiplied; point.y = y * graph.factor; // TODO this is wrong. we need to take GraphOperation into account
this.chart.update(); this.chart.update();
} }
} else { } else {
dataset.data.push({x: x, y: yMultiplied}); // TODO check if this is a LIVE/SCROLLING plot (right end of plot is 'now') dataset.data.push({x: x, y: y}); // TODO check if this is a LIVE/SCROLLING plot (right end of plot is 'now')
this.chart.update(); this.chart.update();
} }
} }

View File

@ -9,10 +9,7 @@ export class Point {
} }
export type PointMapper = (p: number[][], factor: number) => Point[]; export function toBool(points: number[][]): Point[] {
// noinspection JSUnusedLocalSymbols
export function toBool(points: number[][], factor: number): Point[] {
const result = []; const result = [];
let postPone: Point | null = null; let postPone: Point | null = null;
for (const p of points) { for (const p of points) {
@ -44,45 +41,45 @@ export function toBool(points: number[][], factor: number): Point[] {
return result; return result;
} }
export function toDelta(points: number[][], factor: number): Point[] { export function toDelta(points: number[][]): Point[] {
const result = []; const result = [];
for (const p of points) { for (const p of points) {
result.push({ result.push({
x: p[0] * 1000, x: p[0] * 1000,
y: (p[1]) * factor, y: (p[1]),
}); });
} }
return result; return result;
} }
export function toMin(points: number[][], factor: number): Point[] { export function toMin(points: number[][]): Point[] {
const result = []; const result = [];
for (const p of points) { for (const p of points) {
result.push({ result.push({
x: p[0] * 1000, x: p[0] * 1000,
y: p[1] * factor, y: p[1],
}); });
} }
return result; return result;
} }
export function toMax(points: number[][], factor: number): Point[] { export function toMax(points: number[][]): Point[] {
const result = []; const result = [];
for (const p of points) { for (const p of points) {
result.push({ result.push({
x: p[0] * 1000, x: p[0] * 1000,
y: p[2] * factor, y: p[2],
}); });
} }
return result; return result;
} }
export function toAvg(points: number[][], factor: number): Point[] { export function toAvg(points: number[][]): Point[] {
const result = []; const result = [];
for (const p of points) { for (const p of points) {
result.push({ result.push({
x: p[0] * 1000, x: p[0] * 1000,
y: p[3] * factor, y: p[3],
}); });
} }
return result; return result;

View File

@ -4,6 +4,7 @@ import {Series} from './Series';
import {Interval} from './Interval'; import {Interval} from './Interval';
import {AllSeriesPointResponse} from './AllSeriesPointResponse'; import {AllSeriesPointResponse} from './AllSeriesPointResponse';
import {AllSeriesPointRequest} from './AllSeriesPointRequest'; import {AllSeriesPointRequest} from './AllSeriesPointRequest';
import {Graph} from "../plot/axis/graph/Graph";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -16,12 +17,16 @@ export class SeriesService extends EntityListService<Series> {
super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName); super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName);
} }
oneSeriesPoints(series: Series, interval: Interval, offset: number, duration: number, next: Next<number[][]>): void { oneSeriesPoints(graph: Graph, next: Next<number[][]>): void {
const request = { const request = {
id: series.id, id: graph.series.id,
interval: interval.name, factor: graph.factor,
offset: offset, operation: graph.operation,
duration: duration, id2: graph.series2?.id,
factor2: graph.factor2,
interval: graph.axis.plot.interval.name,
offset: graph.axis.plot.offset,
duration: graph.axis.plot.duration,
}; };
this.api.postList([...this.path, 'oneSeriesPoints'], request, (outer: any[]) => outer.map(validateNumber), next); this.api.postList([...this.path, 'oneSeriesPoints'], request, (outer: any[]) => outer.map(validateNumber), next);
} }

View File

@ -43,7 +43,6 @@ public class DemoService {
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void init() { public void init() {
topics(); topics();
// plots();
} }
private void topics() { private void topics() {
@ -168,6 +167,7 @@ public class DemoService {
zuhauseEnergie(); zuhauseEnergie();
zuhauseTemperatur(); zuhauseTemperatur();
eltern(); eltern();
leistungVergleich();
} }
private void zuhauseEnergie() { private void zuhauseEnergie() {
@ -181,30 +181,33 @@ public class DemoService {
energy.setName("Energie"); energy.setName("Energie");
energy.setUnit("kWh"); energy.setUnit("kWh");
final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow(); final String stack = "a";
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack("a");
electricityEnergyPurchaseGraph.setName("Bezug");
electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph);
final Series electricityEnergyDelivery = seriesRepository.findByName("electricity/energy/delivery").orElseThrow(); final Series electricityEnergyDelivery = seriesRepository.findByName("electricity/energy/delivery").orElseThrow();
final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery));
electricityEnergyDeliveryGraph.setType(GraphType.BAR); electricityEnergyDeliveryGraph.setType(GraphType.BAR);
electricityEnergyDeliveryGraph.setStack("a"); electricityEnergyDeliveryGraph.setStack(stack);
electricityEnergyDeliveryGraph.setName("Überschuss"); electricityEnergyDeliveryGraph.setName("Zuhause Überschuss");
electricityEnergyDeliveryGraph.setColor("#FF00FF"); electricityEnergyDeliveryGraph.setColor("#FF00FF");
electricityEnergyDeliveryGraph.setFactor(-1); electricityEnergyDeliveryGraph.setFactor(-1);
energy.addGraph(electricityEnergyDeliveryGraph); energy.addGraph(electricityEnergyDeliveryGraph);
final Series electricityEnergyProduce = seriesRepository.findByName("electricity/energy/produce").orElseThrow(); final Series electricityEnergyProduce = seriesRepository.findByName("electricity/energy/produce").orElseThrow();
final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce));
electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery);
electricityEnergyProduceGraph.setName("Zuhause Eigenverbrauch");
electricityEnergyProduceGraph.setType(GraphType.BAR); electricityEnergyProduceGraph.setType(GraphType.BAR);
electricityEnergyProduceGraph.setStack("a"); electricityEnergyProduceGraph.setStack(stack);
electricityEnergyProduceGraph.setName("Produktion"); electricityEnergyProduceGraph.setColor("#008800");
electricityEnergyProduceGraph.setColor("#0000FF");
energy.addGraph(electricityEnergyProduceGraph); energy.addGraph(electricityEnergyProduceGraph);
final Series electricityEnergyPurchase = seriesRepository.findByName("electricity/energy/purchase").orElseThrow();
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack(stack);
electricityEnergyPurchaseGraph.setName("Zuhause Bezug");
electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph);
} }
private void zuhauseTemperatur() { private void zuhauseTemperatur() {
@ -236,6 +239,24 @@ public class DemoService {
gardenTemperatureGraph.setAvg(false); gardenTemperatureGraph.setAvg(false);
gardenTemperatureGraph.setMax(true); gardenTemperatureGraph.setMax(true);
temperature.addGraph(gardenTemperatureGraph); temperature.addGraph(gardenTemperatureGraph);
final Series bufferTemperature = seriesRepository.findByName("heating/buffer/inside/temperature").orElseThrow();
final Graph bufferTemperatureGraph = graphRepository.save(new Graph(temperature, bufferTemperature));
bufferTemperatureGraph.setName("Puffer");
bufferTemperatureGraph.setColor("#FF00FF");
bufferTemperatureGraph.setMin(true);
bufferTemperatureGraph.setAvg(false);
bufferTemperatureGraph.setMax(true);
temperature.addGraph(bufferTemperatureGraph);
final Series circuitTemperature = seriesRepository.findByName("heating/circuit/supply/temperature").orElseThrow();
final Graph circuitTemperatureGraph = graphRepository.save(new Graph(temperature, circuitTemperature));
circuitTemperatureGraph.setName("Heizkreis");
circuitTemperatureGraph.setColor("#FF0000");
circuitTemperatureGraph.setMin(true);
circuitTemperatureGraph.setAvg(false);
circuitTemperatureGraph.setMax(true);
temperature.addGraph(circuitTemperatureGraph);
} }
private void eltern() { private void eltern() {
@ -249,14 +270,6 @@ public class DemoService {
energy.setName("Energie"); energy.setName("Energie");
energy.setUnit("kWh"); energy.setUnit("kWh");
final Series electricityEnergyPurchase = seriesRepository.findByName("eltern/electricity/energy/purchase").orElseThrow();
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setName("Bezug");
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack("a");
electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph);
final Series electricityEnergyDelivery = seriesRepository.findByName("eltern/electricity/energy/delivery").orElseThrow(); final Series electricityEnergyDelivery = seriesRepository.findByName("eltern/electricity/energy/delivery").orElseThrow();
final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery)); final Graph electricityEnergyDeliveryGraph = graphRepository.save(new Graph(energy, electricityEnergyDelivery));
electricityEnergyDeliveryGraph.setName("Überschuss"); electricityEnergyDeliveryGraph.setName("Überschuss");
@ -268,11 +281,46 @@ public class DemoService {
final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow(); final Series electricityEnergyProduce = seriesRepository.findByName("eltern/electricity/energy/produce").orElseThrow();
final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce)); final Graph electricityEnergyProduceGraph = graphRepository.save(new Graph(energy, electricityEnergyProduce));
electricityEnergyProduceGraph.setName("Produktion"); electricityEnergyProduceGraph.setSeries2(electricityEnergyDelivery);
electricityEnergyProduceGraph.setName("Eigenverbrauch");
electricityEnergyProduceGraph.setType(GraphType.BAR); electricityEnergyProduceGraph.setType(GraphType.BAR);
electricityEnergyProduceGraph.setStack("a"); electricityEnergyProduceGraph.setStack("a");
electricityEnergyProduceGraph.setColor("#0000FF"); electricityEnergyProduceGraph.setColor("#008800");
energy.addGraph(electricityEnergyProduceGraph); energy.addGraph(electricityEnergyProduceGraph);
final Series electricityEnergyPurchase = seriesRepository.findByName("eltern/electricity/energy/purchase").orElseThrow();
final Graph electricityEnergyPurchaseGraph = graphRepository.save(new Graph(energy, electricityEnergyPurchase));
electricityEnergyPurchaseGraph.setName("Bezug");
electricityEnergyPurchaseGraph.setType(GraphType.BAR);
electricityEnergyPurchaseGraph.setStack("a");
electricityEnergyPurchaseGraph.setColor("#FF8800");
energy.addGraph(electricityEnergyPurchaseGraph);
}
private void leistungVergleich() {
final Plot plot = plotRepository.save(new Plot(plotRepository.count()));
plot.setName("Leistung Vergleich");
final Axis power = axisRepository.save(new Axis(plot));
plot.addAxis(power);
plot.setDashboard(true);
power.setRight(true);
power.setName("Leistung");
power.setUnit("W");
final Series electricityPowerProduce = seriesRepository.findByName("electricity/power/produce").orElseThrow();
final Graph electricityPowerProduceGraph = graphRepository.save(new Graph(power, electricityPowerProduce));
electricityPowerProduceGraph.setName("Zuhause");
electricityPowerProduceGraph.setType(GraphType.BAR);
electricityPowerProduceGraph.setColor("#008800");
power.addGraph(electricityPowerProduceGraph);
final Series elternElectricityPowerProduce = seriesRepository.findByName("eltern/electricity/power/produce").orElseThrow();
final Graph elternElectricityPowerProduceGraph = graphRepository.save(new Graph(power, elternElectricityPowerProduce));
elternElectricityPowerProduceGraph.setName("Eltern");
elternElectricityPowerProduceGraph.setType(GraphType.BAR);
elternElectricityPowerProduceGraph.setColor("#0088FF");
power.addGraph(elternElectricityPowerProduceGraph);
} }
@NonNull @NonNull

View File

@ -3,6 +3,7 @@ package de.ph87.data;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.NonNull; import lombok.NonNull;
import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
public class Helpers { public class Helpers {
@ -15,6 +16,14 @@ public class Helpers {
return mapper.apply(t); return mapper.apply(t);
} }
@Nullable
public static <T, U, R> R map(@Nullable final T t, @NonNull final U u, @NonNull final BiFunction<T, U, R> mapper) {
if (t == null) {
return null;
}
return mapper.apply(t, u);
}
@NonNull @NonNull
public static <T> T or(@Nullable final T t, @NonNull final T r) { public static <T> T or(@Nullable final T t, @NonNull final T r) {
if (t == null) { if (t == null) {

View File

@ -2,6 +2,7 @@ package de.ph87.data.plot.axis.graph;
import de.ph87.data.plot.axis.Axis; import de.ph87.data.plot.axis.Axis;
import de.ph87.data.series.Series; import de.ph87.data.series.Series;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
@ -41,6 +42,24 @@ public class Graph {
@ManyToOne(optional = false) @ManyToOne(optional = false)
private Series series; private Series series;
@Setter
@Column(nullable = false)
private double factor = 1;
@Setter
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private GraphOperation operation = GraphOperation.MINUS;
@Setter
@Nullable
@ManyToOne
private Series series2;
@Setter
@Column(nullable = false)
private double factor2 = 1;
@Setter @Setter
@NonNull @NonNull
@Column(nullable = false) @Column(nullable = false)
@ -66,10 +85,6 @@ public class Graph {
@Column(nullable = false) @Column(nullable = false)
private String color = "#FF0000"; private String color = "#FF0000";
@Setter
@Column(nullable = false)
private double factor = 1;
@Setter @Setter
@NonNull @NonNull
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)

View File

@ -56,16 +56,31 @@ public class GraphController {
return graphService.set(id, graph -> graph.setSeries(seriesRepository.findById(value).orElseThrow())); return graphService.set(id, graph -> graph.setSeries(seriesRepository.findById(value).orElseThrow()));
} }
@PostMapping("{id}/color")
public PlotDto color(@PathVariable final long id, @RequestBody @NonNull final String value) {
return graphService.set(id, graph -> graph.setColor(value));
}
@PostMapping("{id}/factor") @PostMapping("{id}/factor")
public PlotDto factor(@PathVariable final long id, @RequestBody final double value) { public PlotDto factor(@PathVariable final long id, @RequestBody final double value) {
return graphService.set(id, graph -> graph.setFactor(value)); return graphService.set(id, graph -> graph.setFactor(value));
} }
@PostMapping("{id}/operation")
public PlotDto operation(@PathVariable final long id, @RequestBody @NonNull final String value) {
return graphService.set(id, graph -> graph.setOperation(GraphOperation.valueOf(value)));
}
@PostMapping("{id}/series2")
public PlotDto series2(@PathVariable final long id, @RequestBody(required = false) @Nullable final Long value) {
return graphService.set(id, graph -> graph.setSeries2(value == null ? null : seriesRepository.findById(value).orElseThrow()));
}
@PostMapping("{id}/factor2")
public PlotDto factor2(@PathVariable final long id, @RequestBody final double value) {
return graphService.set(id, graph -> graph.setFactor2(value));
}
@PostMapping("{id}/color")
public PlotDto color(@PathVariable final long id, @RequestBody @NonNull final String value) {
return graphService.set(id, graph -> graph.setColor(value));
}
@PostMapping("{id}/group") @PostMapping("{id}/group")
public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String value) { public PlotDto group(@PathVariable final long id, @RequestBody @NonNull final String value) {
return graphService.set(id, graph -> graph.setGroup(Group.valueOf(value))); return graphService.set(id, graph -> graph.setGroup(Group.valueOf(value)));

View File

@ -0,0 +1,5 @@
package de.ph87.data.plot.axis.graph;
public class GraphDivisionByZero extends Exception {
}

View File

@ -1,9 +1,12 @@
package de.ph87.data.plot.axis.graph; package de.ph87.data.plot.axis.graph;
import de.ph87.data.series.SeriesDto; import de.ph87.data.series.SeriesDto;
import jakarta.annotation.Nullable;
import lombok.Data; import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import static de.ph87.data.Helpers.map;
@Data @Data
public class GraphDto { public class GraphDto {
@ -14,6 +17,15 @@ public class GraphDto {
@NonNull @NonNull
public final SeriesDto series; public final SeriesDto series;
public final double factor;
public final GraphOperation operation;
@Nullable
public final SeriesDto series2;
public final double factor2;
@NonNull @NonNull
public final String name; public final String name;
@ -26,8 +38,6 @@ public class GraphDto {
@NonNull @NonNull
public final String color; public final String color;
public final double factor;
@NonNull @NonNull
public final Group group; public final Group group;
@ -44,12 +54,15 @@ public class GraphDto {
this.id = graph.getId(); this.id = graph.getId();
this.version = graph.getVersion(); this.version = graph.getVersion();
this.series = new SeriesDto(graph.getSeries(), false); this.series = new SeriesDto(graph.getSeries(), false);
this.factor = graph.getFactor();
this.operation = graph.getOperation();
this.series2 = map(graph.getSeries2(), false, SeriesDto::new);
this.factor2 = graph.getFactor2();
this.name = graph.getName(); this.name = graph.getName();
this.visible = graph.isVisible(); this.visible = graph.isVisible();
this.type = graph.getType(); this.type = graph.getType();
this.fill = graph.getFill(); this.fill = graph.getFill();
this.color = graph.getColor(); this.color = graph.getColor();
this.factor = graph.getFactor();
this.group = graph.getGroup(); this.group = graph.getGroup();
this.stack = graph.getStack(); this.stack = graph.getStack();
this.min = graph.isMin(); this.min = graph.isMin();

View File

@ -0,0 +1,27 @@
package de.ph87.data.plot.axis.graph;
import lombok.NonNull;
public enum GraphOperation {
MINUS((s0, s1) -> s0 - s1),
PLUS(Double::sum),
DIVIDE((s0, s1) -> {
if (s1 == 0) {
throw new GraphDivisionByZero();
}
return s0 / s1;
}),
;
@NonNull
private final GraphOperationFunction<Double, Double, Double> function;
GraphOperation(@NonNull final GraphOperationFunction<Double, Double, Double> function) {
this.function = function;
}
public double apply(final double a, final double b) throws GraphDivisionByZero {
return function.apply(a, b);
}
}

View File

@ -0,0 +1,8 @@
package de.ph87.data.plot.axis.graph;
@FunctionalInterface
public interface GraphOperationFunction<A, B, R> {
R apply(A a, B b) throws GraphDivisionByZero;
}

View File

@ -22,7 +22,7 @@ public class AllSeriesPointResponse {
public final SeriesDto series; public final SeriesDto series;
@Nullable @Nullable
public final SeriesPoint point; public final SeriesPoint<?> point;
} }

View File

@ -2,7 +2,9 @@ package de.ph87.data.series;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.data.Interval; import de.ph87.data.series.data.Interval;
import jakarta.annotation.Nullable;
import lombok.Data; import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
@ -13,6 +15,15 @@ public class OneSeriesPointsRequest implements ISeriesPointRequest {
public final long id; public final long id;
public final double factor;
public final GraphOperation operation;
@Nullable
public final Long id2;
public final double factor2;
@NonNull @NonNull
public final Interval interval; public final Interval interval;
@ -30,11 +41,19 @@ public class OneSeriesPointsRequest implements ISeriesPointRequest {
public OneSeriesPointsRequest( public OneSeriesPointsRequest(
@JsonProperty("id") final long id, @JsonProperty("id") final long id,
@JsonProperty("factor") final double factor,
@JsonProperty("operation") final GraphOperation operation,
@JsonProperty("id2") @Nullable final Long id2,
@JsonProperty("factor2") final double factor2,
@JsonProperty("interval") final Interval interval, @JsonProperty("interval") final Interval interval,
@JsonProperty("offset") final long offset, @JsonProperty("offset") final long offset,
@JsonProperty("duration") final long duration @JsonProperty("duration") final long duration
) { ) {
this.id = id; this.id = id;
this.factor = factor;
this.operation = operation;
this.id2 = id2;
this.factor2 = factor2;
this.interval = interval; this.interval = interval;
this.offset = offset; this.offset = offset;
this.duration = duration; this.duration = duration;

View File

@ -9,6 +9,6 @@ import java.util.List;
@JsonSerialize(using = OneSeriesPointsResponseSerializer.class) @JsonSerialize(using = OneSeriesPointsResponseSerializer.class)
public class OneSeriesPointsResponse { public class OneSeriesPointsResponse {
public final List<? extends SeriesPoint> points; public final List<? extends SeriesPoint<?>> points;
} }

View File

@ -1,11 +1,67 @@
package de.ph87.data.series; package de.ph87.data.series;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import lombok.NonNull;
import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public interface SeriesPoint { public interface SeriesPoint<T extends SeriesPoint<T>> {
ZonedDateTime getDate();
double getValue();
void toJson(final JsonGenerator jsonGenerator) throws IOException; void toJson(final JsonGenerator jsonGenerator) throws IOException;
T times(final double factor);
T combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero;
@NonNull
static List<SeriesPoint<?>> combine(@NonNull final List<? extends SeriesPoint<?>> points1, @NonNull final List<? extends SeriesPoint<?>> points2, @NonNull final GraphOperation operation) {
if (points1.isEmpty() || points2.isEmpty()) {
return Collections.emptyList();
}
final List<SeriesPoint<?>> as = new ArrayList<>(points1);
final List<SeriesPoint<?>> bs = new ArrayList<>(points2);
SeriesPoint<?> a = as.removeFirst();
SeriesPoint<?> b = bs.removeFirst();
final List<SeriesPoint<?>> result = new ArrayList<>(Math.min(as.size(), bs.size()));
while (true) {
final int diff = a.getDate().compareTo(b.getDate());
if (diff == 0) {
try {
result.add(a.combine(b, operation));
} catch (GraphDivisionByZero e) {
// just not add
}
a = null;
b = null;
} else if (diff < 0) {
a = null;
} else {
b = null;
}
if (a == null) {
if (as.isEmpty()) {
break;
}
a = as.removeFirst();
}
if (b == null) {
if (bs.isEmpty()) {
break;
}
b = bs.removeFirst();
}
}
return result;
}
} }

View File

@ -3,6 +3,7 @@ package de.ph87.data.series;
import de.ph87.data.series.data.bool.BoolService; import de.ph87.data.series.data.bool.BoolService;
import de.ph87.data.series.data.delta.DeltaService; import de.ph87.data.series.data.delta.DeltaService;
import de.ph87.data.series.data.varying.VaryingService; import de.ph87.data.series.data.varying.VaryingService;
import jakarta.annotation.Nullable;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -27,8 +28,14 @@ public class SeriesService {
@NonNull @NonNull
public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) { public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) {
final Series series = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); final Series series1 = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new OneSeriesPointsResponse(getSeriesPoints(series, request)); final List<? extends SeriesPoint<?>> points1 = getSeriesPoints(series1, request, request.factor);
if (request.id2 != null) {
final Series series2 = seriesRepository.findById(request.id2).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
final List<? extends SeriesPoint<?>> points2 = getSeriesPoints(series2, request, request.factor2);
return new OneSeriesPointsResponse(SeriesPoint.combine(points1, points2, request.operation));
}
return new OneSeriesPointsResponse(points1);
} }
@NonNull @NonNull
@ -39,19 +46,23 @@ public class SeriesService {
@NonNull @NonNull
private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
final List<? extends SeriesPoint> points = getSeriesPoints(series, request); final List<? extends SeriesPoint<?>> points = getSeriesPoints(series, request, null);
final SeriesDto seriesDto = new SeriesDto(series, false); final SeriesDto seriesDto = new SeriesDto(series, false);
final SeriesPoint point = points.isEmpty() ? null : points.getFirst(); final SeriesPoint<?> point = points.isEmpty() ? null : points.getFirst();
return new AllSeriesPointResponse.Entry(seriesDto, point); return new AllSeriesPointResponse.Entry(seriesDto, point);
} }
@NonNull @NonNull
private List<? extends SeriesPoint> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { private List<? extends SeriesPoint<?>> getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request, @Nullable final Double factor) {
return switch (series.getType()) { final List<? extends SeriesPoint<?>> points = switch (series.getType()) {
case BOOL -> boolService.points(series, request); case BOOL -> boolService.points(series, request);
case DELTA -> deltaService.points(series, request); case DELTA -> deltaService.points(series, request);
case VARYING -> varyingService.points(series, request); case VARYING -> varyingService.points(series, request);
}; };
if (factor == null || factor == 1) {
return points;
}
return points.stream().map(p -> p.times(factor)).toList();
} }
} }

View File

@ -1,14 +1,17 @@
package de.ph87.data.series.data.bool; package de.ph87.data.series.data.bool;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.SeriesPoint; import de.ph87.data.series.SeriesPoint;
import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@SuppressWarnings("unused") // used by repository query @Data
public class BoolPoint implements SeriesPoint { public class BoolPoint implements SeriesPoint<BoolPoint> {
public final ZonedDateTime begin; public final ZonedDateTime begin;
@ -25,6 +28,13 @@ public class BoolPoint implements SeriesPoint {
this.terminated = bool.isTerminated(); this.terminated = bool.isTerminated();
} }
public BoolPoint(final ZonedDateTime begin, final ZonedDateTime end, final boolean state, final boolean terminated) {
this.begin = begin;
this.end = end;
this.state = state;
this.terminated = terminated;
}
@Override @Override
public void toJson(final JsonGenerator jsonGenerator) throws IOException { public void toJson(final JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeNumber(begin.toEpochSecond()); jsonGenerator.writeNumber(begin.toEpochSecond());
@ -33,4 +43,24 @@ public class BoolPoint implements SeriesPoint {
jsonGenerator.writeNumber(terminated ? 1 : 0); jsonGenerator.writeNumber(terminated ? 1 : 0);
} }
@Override
public BoolPoint times(final double factor) {
return this;
}
@Override
public ZonedDateTime getDate() {
return begin;
}
@Override
public double getValue() {
return state ? 1 : 0;
}
@Override
public BoolPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
return new BoolPoint(begin, end, operation.apply(getValue(), other.getValue()) > 0, terminated);
}
} }

View File

@ -1,14 +1,17 @@
package de.ph87.data.series.data.delta; package de.ph87.data.series.data.delta;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.SeriesPoint; import de.ph87.data.series.SeriesPoint;
import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@SuppressWarnings("unused") // used by repository query @Data
public class DeltaPoint implements SeriesPoint { public class DeltaPoint implements SeriesPoint<DeltaPoint> {
@NonNull @NonNull
public final ZonedDateTime date; public final ZonedDateTime date;
@ -26,4 +29,19 @@ public class DeltaPoint implements SeriesPoint {
jsonGenerator.writeNumber(delta); jsonGenerator.writeNumber(delta);
} }
@Override
public DeltaPoint times(final double factor) {
return new DeltaPoint(date, delta * factor);
}
@Override
public double getValue() {
return delta;
}
@Override
public DeltaPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
return new DeltaPoint(date, operation.apply(getValue(), other.getValue()));
}
} }

View File

@ -13,7 +13,7 @@ import java.util.List;
public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DeltaId> { public interface DeltaRepo<T extends Delta> extends CrudRepository<T, DeltaId> {
@NonNull @NonNull
@Query("select new de.ph87.data.series.data.delta.DeltaPoint(e.id.date, sum(e.last - e.first)) from #{#entityName} e where e.id.meter.series = :series and e.id.date >= :first and e.id.date < :after group by e.id.date") @Query("select new de.ph87.data.series.data.delta.DeltaPoint(e.id.date, sum((e.last - e.first))) from #{#entityName} e where e.id.meter.series = :series and e.id.date >= :first and e.id.date < :after group by e.id.date")
List<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after); List<DeltaPoint> points(@NonNull Series series, @NonNull ZonedDateTime first, @NonNull ZonedDateTime after);
} }

View File

@ -1,14 +1,17 @@
package de.ph87.data.series.data.varying; package de.ph87.data.series.data.varying;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import de.ph87.data.plot.axis.graph.GraphDivisionByZero;
import de.ph87.data.plot.axis.graph.GraphOperation;
import de.ph87.data.series.SeriesPoint; import de.ph87.data.series.SeriesPoint;
import lombok.Data;
import lombok.NonNull; import lombok.NonNull;
import java.io.IOException; import java.io.IOException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@SuppressWarnings("unused") // used by repository query @Data
public class VaryingPoint implements SeriesPoint { public class VaryingPoint implements SeriesPoint<VaryingPoint> {
public final ZonedDateTime date; public final ZonedDateTime date;
@ -25,6 +28,13 @@ public class VaryingPoint implements SeriesPoint {
this.avg = varying.getAvg(); this.avg = varying.getAvg();
} }
private VaryingPoint(final ZonedDateTime date, final double min, final double avg, final double max) {
this.date = date;
this.min = min;
this.avg = avg;
this.max = max;
}
@Override @Override
public void toJson(final JsonGenerator jsonGenerator) throws IOException { public void toJson(final JsonGenerator jsonGenerator) throws IOException {
jsonGenerator.writeNumber(date.toEpochSecond()); jsonGenerator.writeNumber(date.toEpochSecond());
@ -33,4 +43,22 @@ public class VaryingPoint implements SeriesPoint {
jsonGenerator.writeNumber(avg); jsonGenerator.writeNumber(avg);
} }
@Override
public VaryingPoint times(final double factor) {
return new VaryingPoint(date, min * factor, avg * factor, max * factor);
}
@Override
public double getValue() {
return avg;
}
@Override
public VaryingPoint combine(@NonNull final SeriesPoint<?> other, @NonNull final GraphOperation operation) throws GraphDivisionByZero {
if (other instanceof final VaryingPoint otherVarying) {
return new VaryingPoint(date, operation.apply(min, otherVarying.min), operation.apply(avg, otherVarying.avg), operation.apply(max, otherVarying.max));
}
return new VaryingPoint(date, operation.apply(min, other.getValue()), operation.apply(avg, other.getValue()), operation.apply(max, other.getValue()));
}
} }