extracted Plot into own component + Dashboard + Graph.type + Graph.fill
This commit is contained in:
parent
53fad7846b
commit
6172e888bf
@ -1,16 +1,19 @@
|
|||||||
import {StompService} from "@stomp/ng2-stompjs";
|
import {StompService} from "@stomp/ng2-stompjs";
|
||||||
import {filter, map, Subscription} from "rxjs";
|
import {filter, map, Subject, Subscription} from "rxjs";
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {RxStompState} from '@stomp/rx-stomp';
|
import {RxStompState} from '@stomp/rx-stomp';
|
||||||
|
|
||||||
export type FromJson<T> = (json: any) => T;
|
export type FromJson<T> = (json: any) => T;
|
||||||
|
|
||||||
export type FromJsonIndexed<T> = (json: any, index: number) => T;
|
export type FromJsonIndexed<T> = (json: any, index: number) => T;
|
||||||
|
|
||||||
export type Next<T> = (item: T) => any;
|
export type Next<T> = (item: T) => any;
|
||||||
|
|
||||||
export type Compare<T> = (a: T, b: T) => number;
|
export type Compare<T> = (a: T, b: T) => number;
|
||||||
|
|
||||||
|
export type Equals<T> = (a: T, b: T) => boolean;
|
||||||
|
|
||||||
export function stompServiceFactory() {
|
export function stompServiceFactory() {
|
||||||
const stomp = new StompService({
|
const stomp = new StompService({
|
||||||
url: url('ws', ['websocket']),
|
url: url('ws', ['websocket']),
|
||||||
@ -194,6 +197,84 @@ export abstract class CrudService<T> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class ID<T extends ID<T>> {
|
||||||
|
|
||||||
|
abstract get id(): number;
|
||||||
|
|
||||||
|
abstract get deleted(): boolean;
|
||||||
|
|
||||||
|
hasId(id: number): boolean {
|
||||||
|
return this.id === id;
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(t: T | null): boolean {
|
||||||
|
return t !== null && t !== undefined && t.hasId(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
equals_(): (t: (T | null)) => boolean {
|
||||||
|
return (t: T | null) => this.equals(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
static equals<X extends ID<X>>(a: X, b: X): boolean {
|
||||||
|
return ID.hasId<X>(a.id)(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static hasId<X extends ID<X>>(id: number): (t: X) => boolean {
|
||||||
|
return (t: X) => t.hasId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class EntityListService<T extends ID<T>> extends CrudService<T> {
|
||||||
|
|
||||||
|
private readonly subject: Subject<T> = new Subject();
|
||||||
|
|
||||||
|
private _list: T[] = [];
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
api: ApiService,
|
||||||
|
path: any[],
|
||||||
|
fromJson: FromJson<T>,
|
||||||
|
readonly equals: Equals<T>,
|
||||||
|
readonly compare: Compare<T>,
|
||||||
|
) {
|
||||||
|
super(api, path, fromJson);
|
||||||
|
this.findAll(all => {
|
||||||
|
this._list = all;
|
||||||
|
all.forEach(t => this.subject.next(t));
|
||||||
|
});
|
||||||
|
this.subscribe(this._update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _update = (fresh: T) => {
|
||||||
|
const index = this._list.findIndex(e => this.equals(e, fresh));
|
||||||
|
if (fresh.deleted) {
|
||||||
|
if (index >= 0) {
|
||||||
|
this._list.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (index >= 0) {
|
||||||
|
this._list[index] = fresh;
|
||||||
|
} else {
|
||||||
|
this._list.push(fresh);
|
||||||
|
}
|
||||||
|
this._list = this._list.sort(this.compare);
|
||||||
|
this.subject.next(fresh);
|
||||||
|
};
|
||||||
|
|
||||||
|
get list(): T[] {
|
||||||
|
return [...this._list];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeListItems(next: Next<T>): Subscription {
|
||||||
|
return this.subject.subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
byId(id: number): T {
|
||||||
|
return this._list.filter(t => t.hasId(id))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export function NTU<T>(v: T | null | undefined): T | undefined {
|
export function NTU<T>(v: T | null | undefined): T | undefined {
|
||||||
if (v === null) {
|
if (v === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -1 +1,17 @@
|
|||||||
|
<div class="sidebar">
|
||||||
|
@if (sidebar) {
|
||||||
|
<div class="content">
|
||||||
|
<div class="item" [routerLink]="['Dashboard']" routerLinkActive="itemActive" (click)="sidebar = false">Dashboard</div>
|
||||||
|
<div class="item" [routerLink]="['PlotEditor']" routerLinkActive="itemActive" (click)="sidebar = false">Diagramm-Editor</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="handle" (click)="sidebar = !sidebar">
|
||||||
|
@if (sidebar) {
|
||||||
|
<
|
||||||
|
} @else {
|
||||||
|
>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-outlet/>
|
<router-outlet/>
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
@sidebarColor: #62b0ca;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
margin-left: -2em;
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
border-bottom-right-radius: 0.5em;
|
||||||
|
background-color: @sidebarColor;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle:hover {
|
||||||
|
background-color: lightyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
background-color: @sidebarColor;
|
||||||
|
width: 300px;
|
||||||
|
max-width: calc(100vw - 1em);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background-color: lightyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActive {
|
||||||
|
background-color: steelblue;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {PlotComponent} from './plot/plot.component';
|
import {PlotEditor} from './plot/editor/plot-editor.component';
|
||||||
|
import {DashboardComponent} from './dashboard/dashboard.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: 'plot', component: PlotComponent},
|
{path: 'Dashboard', component: DashboardComponent},
|
||||||
{path: 'plot/:id', component: PlotComponent},
|
{path: 'PlotEditor', component: PlotEditor},
|
||||||
{path: '**', redirectTo: 'plot'},
|
{path: 'PlotEditor/:id', component: PlotEditor},
|
||||||
|
{path: '**', redirectTo: 'Dashboard'},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import {Component, signal} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {RouterOutlet} from '@angular/router';
|
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterOutlet, RouterLink, RouterLinkActive],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.less'
|
styleUrl: './app.less'
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected readonly title = signal('angular');
|
|
||||||
|
protected sidebar: boolean = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
@for (plot of plots; track plot.id) {
|
||||||
|
<div class="plot">
|
||||||
|
<app-plot [plot]="plot"></app-plot>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
.plot {
|
||||||
|
height: 60vw;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
26
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal file
26
src/main/angular/src/app/dashboard/dashboard.component.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {PlotService} from '../plot/plot.service';
|
||||||
|
import {Plot} from '../plot/Plot';
|
||||||
|
import {PlotComponent} from '../plot/plot/plot.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
imports: [
|
||||||
|
PlotComponent
|
||||||
|
],
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrl: './dashboard.component.less'
|
||||||
|
})
|
||||||
|
export class DashboardComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly plotService: PlotService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get plots(): Plot[] {
|
||||||
|
return this.plotService.list.filter(p => p.dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import {validateBoolean, validateListIndexed, validateNumber, validateString} from '../COMMON';
|
import {ID, validateBoolean, validateListIndexed, validateNumber, validateString} from '../COMMON';
|
||||||
import {Interval} from '../series/Interval';
|
import {Interval} from '../series/Interval';
|
||||||
import {Axis} from './Axis';
|
import {Axis} from './axis/Axis';
|
||||||
|
|
||||||
export class Plot {
|
export class Plot extends ID<Plot> {
|
||||||
|
|
||||||
readonly axes: Axis[];
|
readonly axes: Axis[];
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ export class Plot {
|
|||||||
readonly position: number,
|
readonly position: number,
|
||||||
axes: any[],
|
axes: any[],
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this.axes = validateListIndexed(axes, ((json, index) => Axis.fromJson(this, index, json)));
|
this.axes = validateListIndexed(axes, ((json, index) => Axis.fromJson(this, index, json)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {mapNotNull, validateBoolean, validateList, validateNumber, validateString} from "../COMMON";
|
import {mapNotNull, validateBoolean, validateList, validateNumber, validateString} from "../../COMMON";
|
||||||
import {Graph} from './Graph';
|
import {Graph} from './graph/Graph';
|
||||||
import {Plot} from './Plot';
|
import {Plot} from '../Plot';
|
||||||
|
|
||||||
export class Axis {
|
export class Axis {
|
||||||
|
|
||||||
@ -1,14 +1,17 @@
|
|||||||
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 {validateBoolean, 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';
|
||||||
|
|
||||||
export class Graph {
|
export class Graph {
|
||||||
|
|
||||||
readonly type: string = "line";
|
readonly showMin: boolean;
|
||||||
|
|
||||||
readonly mid: boolean;
|
readonly showMid: boolean;
|
||||||
|
|
||||||
|
readonly showMax: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly axis: Axis,
|
readonly axis: Axis,
|
||||||
@ -17,6 +20,8 @@ export class Graph {
|
|||||||
readonly series: Series,
|
readonly series: Series,
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly visible: boolean,
|
readonly visible: boolean,
|
||||||
|
readonly type: GraphType,
|
||||||
|
readonly fill: string,
|
||||||
readonly color: string,
|
readonly color: string,
|
||||||
readonly factor: number,
|
readonly factor: number,
|
||||||
readonly group: Group,
|
readonly group: Group,
|
||||||
@ -25,7 +30,9 @@ export class Graph {
|
|||||||
readonly max: boolean,
|
readonly max: boolean,
|
||||||
readonly avg: boolean,
|
readonly avg: boolean,
|
||||||
) {
|
) {
|
||||||
this.mid = this.avg || this.series.type === SeriesType.BOOL || this.series.type === SeriesType.DELTA;
|
this.showMin = this.series.type === SeriesType.VARYING && this.min;
|
||||||
|
this.showMid = this.series.type === SeriesType.BOOL || this.series.type === SeriesType.DELTA || this.avg;
|
||||||
|
this.showMax = this.series.type === SeriesType.VARYING && this.max;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(axis: Axis, json: any): Graph {
|
static fromJson(axis: Axis, json: any): Graph {
|
||||||
@ -36,6 +43,8 @@ export class Graph {
|
|||||||
Series.fromJson(json.series),
|
Series.fromJson(json.series),
|
||||||
validateString(json.name),
|
validateString(json.name),
|
||||||
validateBoolean(json.visible),
|
validateBoolean(json.visible),
|
||||||
|
GraphType.fromJson(json.type),
|
||||||
|
validateString(json.fill),
|
||||||
validateString(json.color),
|
validateString(json.color),
|
||||||
validateNumber(json.factor),
|
validateNumber(json.factor),
|
||||||
validateString(json.group) as Group,
|
validateString(json.group) as Group,
|
||||||
32
src/main/angular/src/app/plot/axis/graph/GraphType.ts
Normal file
32
src/main/angular/src/app/plot/axis/graph/GraphType.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {validateString} from "../../../COMMON";
|
||||||
|
|
||||||
|
export class GraphType {
|
||||||
|
|
||||||
|
protected static _values: GraphType[] = [];
|
||||||
|
|
||||||
|
static readonly LINE = new GraphType("LINE", "line", "Linie");
|
||||||
|
|
||||||
|
static readonly BAR = new GraphType("BAR", "bar", "Balken");
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
readonly jsonName: string,
|
||||||
|
readonly chartJsName: "line" | "bar",
|
||||||
|
readonly display: string,
|
||||||
|
) {
|
||||||
|
GraphType._values.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): GraphType {
|
||||||
|
const name = validateString(json)
|
||||||
|
const graphType = GraphType._values.filter(i => i.jsonName === name)[0];
|
||||||
|
if (!graphType) {
|
||||||
|
throw new Error(`Not an GraphType: ${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
|
return graphType;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get values(): GraphType[] {
|
||||||
|
return GraphType._values;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,23 +1,23 @@
|
|||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<select [ngModel]="plot" (ngModelChange)="buildPlot($event)">
|
<select [ngModel]="plot" (ngModelChange)="plot = $event">
|
||||||
@for (p of plotList; track p.id) {
|
@for (p of plotService.list; track p.id) {
|
||||||
<option [ngValue]="p">#{{ p.position }}: {{ p.name || '---' }}</option>
|
<option [ngValue]="p">#{{ p.position }}: {{ p.name || '---' }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<button (click)="plotService.plotCreate(updateAndBuild);">
|
<button class="button buttonAdd" (click)="plotService.plotCreate(setPlot)">
|
||||||
<fa-icon [icon]="faPlus"></fa-icon>
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<button (click)="plot && plotService.plotDuplicate(plot, updateAndBuild)" [disabled]="!plot">
|
<button class="button buttonCopy" (click)="plot && plotService.plotDuplicate(plot, setPlot)" [disabled]="!plot">
|
||||||
<fa-icon [icon]="faCopy"></fa-icon>
|
<fa-icon [icon]="faCopy"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
<button (click)="plot && plotService.plotDelete(plot, updatePlot)" [disabled]="!plot">
|
<button class="button buttonRemove" (click)="plot && plotService.plotDelete(plot)" [disabled]="!plot">
|
||||||
<fa-icon [icon]="faTrash"></fa-icon>
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div #container class="container">
|
<div class="plot">
|
||||||
<canvas #chartCanvas></canvas>
|
<app-plot [plot]="plot"></app-plot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (plot) {
|
@if (plot) {
|
||||||
@ -34,26 +34,26 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="plot.name" (onChange)="plotService.plotName(plot, $event, updatePlot)"></app-text>
|
<app-text [initial]="plot.name" (onChange)="plotService.plotName(plot, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select [ngModel]="plot.interval" (ngModelChange)="plotService.plotInterval(plot, $event, updatePlot)">
|
<select [ngModel]="plot.interval" (ngModelChange)="plotService.plotInterval(plot, $event)">
|
||||||
@for (interval of Interval.values; track interval) {
|
@for (interval of Interval.values; track interval) {
|
||||||
<option [ngValue]="interval">{{ interval.display }}</option>
|
<option [ngValue]="interval">{{ interval.display }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number-nn [initial]="plot.offset" (onChange)="plotService.plotOffset(plot, $event, updatePlot)"></app-number-nn>
|
<app-number-nn [initial]="plot.offset" (onChange)="plotService.plotOffset(plot, $event)"></app-number-nn>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number-nn [initial]="plot.duration" (onChange)="plotService.plotDuration(plot, $event, updatePlot)"></app-number-nn>
|
<app-number-nn [initial]="plot.duration" (onChange)="plotService.plotDuration(plot, $event)"></app-number-nn>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="plot.dashboard" (onChange)="plotService.plotDashboard(plot, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="plot.dashboard" (onChange)="plotService.plotDashboard(plot, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number-nn [initial]="plot.position" (onChange)="plotService.plotPosition(plot, $event, updatePlot)"></app-number-nn>
|
<app-number-nn [initial]="plot.position" (onChange)="plotService.plotPosition(plot, $event)"></app-number-nn>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@ -81,48 +81,46 @@
|
|||||||
@for (axis of plot.axes; track axis.id) {
|
@for (axis of plot.axes; track axis.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="axis.visible" (onChange)="plotService.axisVisible(axis, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="axis.visible" (onChange)="plotService.axisVisible(axis, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
Y{{ axis.index + 1 }}
|
Y{{ axis.index + 1 }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="axis.name" (onChange)="plotService.axisName(axis, $event, updatePlot)"></app-text>
|
<app-text [initial]="axis.name" (onChange)="plotService.axisName(axis, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="axis.unit" (onChange)="plotService.axisUnit(axis, $event, updatePlot)"></app-text>
|
<app-text [initial]="axis.unit" (onChange)="plotService.axisUnit(axis, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="axis.right" (onChange)="plotService.axisRight(axis, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="axis.right" (onChange)="plotService.axisRight(axis, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number [initial]="axis.min" (onChange)="plotService.axisMin(axis, $event, updatePlot)"></app-number>
|
<app-number [initial]="axis.min" (onChange)="plotService.axisMin(axis, $event)"></app-number>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="axis.minHard" (onChange)="plotService.axisMinHard(axis, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="axis.minHard" (onChange)="plotService.axisMinHard(axis, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number [initial]="axis.max" (onChange)="plotService.axisMax(axis, $event, updatePlot)"></app-number>
|
<app-number [initial]="axis.max" (onChange)="plotService.axisMax(axis, $event)"></app-number>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="axis.maxHard" (onChange)="plotService.axisMaxHard(axis, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="axis.maxHard" (onChange)="plotService.axisMaxHard(axis, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button (click)="plotService.axisDelete(axis, updatePlot)">
|
<button class="button buttonRemove" (click)="plotService.axisDelete(axis)">
|
||||||
<fa-icon [icon]="faTrash"></fa-icon>
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
Achse
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button (click)="plotService.axisAddGraph(axis, updatePlot)">
|
<button class="button buttonAdd" (click)="plotService.axisAddGraph(axis)">
|
||||||
<fa-icon [icon]="faPlus"></fa-icon>
|
<fa-icon [icon]="faChartLine"></fa-icon>
|
||||||
Grafen
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<button (click)="plotService.plotAddAxis(plot, updatePlot)">
|
<button class="button buttonAdd" (click)="plotService.plotAddAxis(plot)">
|
||||||
<fa-icon [icon]="faPlus"></fa-icon>
|
<fa-icon [icon]="faPlus"></fa-icon>
|
||||||
Achse
|
Achse
|
||||||
</button>
|
</button>
|
||||||
@ -138,6 +136,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Serie</th>
|
<th>Serie</th>
|
||||||
|
<th>Typ</th>
|
||||||
<th>Farbe</th>
|
<th>Farbe</th>
|
||||||
<th>Faktor</th>
|
<th>Faktor</th>
|
||||||
<th>Aggregat</th>
|
<th>Aggregat</th>
|
||||||
@ -152,43 +151,50 @@
|
|||||||
@for (graph of axis.graphs; track graph.id) {
|
@for (graph of axis.graphs; track graph.id) {
|
||||||
<tr [style.background-color]="graph.color">
|
<tr [style.background-color]="graph.color">
|
||||||
<td>
|
<td>
|
||||||
<app-checkbox [initial]="graph.visible" (onChange)="plotService.graphVisible(graph, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="graph.visible" (onChange)="plotService.graphVisible(graph, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="graph.name" (onChange)="plotService.graphName(graph, $event, updatePlot)"></app-text>
|
<app-text [initial]="graph.name" (onChange)="plotService.graphName(graph, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select [ngModel]="graph.series.id" (ngModelChange)="plotService.graphSeries(graph, $event, updatePlot)">
|
<select [ngModel]="graph.series.id" (ngModelChange)="plotService.graphSeries(graph, $event)">
|
||||||
@for (s of seriesList; track s.id) {
|
@for (s of seriesService.list; track s.id) {
|
||||||
<option [ngValue]="s.id">{{ s.name }} [{{ s.valueString }}]</option>
|
<option [ngValue]="s.id">{{ s.name }} [{{ s.valueString }}]</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="graph.color" (onChange)="plotService.graphColor(graph, $event, updatePlot)"></app-text>
|
<select [ngModel]="graph.type" (ngModelChange)="plotService.graphType(graph, $event)">
|
||||||
|
@for (type of GraphType.values; track type.jsonName) {
|
||||||
|
<option [ngValue]="type">{{ type.display }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-number-nn [initial]="graph.factor" (onChange)="plotService.graphFactor(graph, $event, updatePlot)"></app-number-nn>
|
<app-text [initial]="graph.color" (onChange)="plotService.graphColor(graph, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select [ngModel]="graph.group" (ngModelChange)="plotService.graphGroup(graph, $event, updatePlot)">
|
<app-number-nn [initial]="graph.factor" (onChange)="plotService.graphFactor(graph, $event)"></app-number-nn>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select [ngModel]="graph.group" (ngModelChange)="plotService.graphGroup(graph, $event)">
|
||||||
@for (group of groups(); track group) {
|
@for (group of groups(); track group) {
|
||||||
<option [ngValue]="group">{{ group }}</option>
|
<option [ngValue]="group">{{ group }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-text [initial]="graph.stack" (onChange)="plotService.graphStack(graph, $event, updatePlot)"></app-text>
|
<app-text [initial]="graph.stack" (onChange)="plotService.graphStack(graph, $event)"></app-text>
|
||||||
</td>
|
</td>
|
||||||
@if (graph.series.type === SeriesType.VARYING) {
|
@if (graph.series.type === SeriesType.VARYING) {
|
||||||
<td class="subSeries">
|
<td class="subSeries">
|
||||||
<app-checkbox [initial]="graph.min" (onChange)="plotService.graphMin(graph, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="graph.min" (onChange)="plotService.graphMin(graph, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td class="subSeries">
|
<td class="subSeries">
|
||||||
<app-checkbox [initial]="graph.avg" (onChange)="plotService.graphAvg(graph, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="graph.avg" (onChange)="plotService.graphAvg(graph, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
<td class="subSeries">
|
<td class="subSeries">
|
||||||
<app-checkbox [initial]="graph.max" (onChange)="plotService.graphMax(graph, $event, updatePlot)"></app-checkbox>
|
<app-checkbox [initial]="graph.max" (onChange)="plotService.graphMax(graph, $event)"></app-checkbox>
|
||||||
</td>
|
</td>
|
||||||
} @else {
|
} @else {
|
||||||
@if (graph.series.type === SeriesType.BOOL) {
|
@if (graph.series.type === SeriesType.BOOL) {
|
||||||
@ -198,14 +204,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
<td>
|
<td>
|
||||||
<select [ngModel]="graph.axis.id" (ngModelChange)="plotService.graphAxis(graph, $event, updatePlot)">
|
<select [ngModel]="graph.axis.id" (ngModelChange)="plotService.graphAxis(graph, $event)">
|
||||||
@for (axis of plot.axes; track axis.id) {
|
@for (axis of plot.axes; track axis.id) {
|
||||||
<option [ngValue]="axis.id">Y{{ axis.index + 1 }} {{ axis.name }}</option>
|
<option [ngValue]="axis.id">Y{{ axis.index + 1 }} {{ axis.name }}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button (click)="plotService.graphDelete(graph, updatePlot)">
|
<button class="button buttonRemove" (click)="plotService.graphDelete(graph)">
|
||||||
<fa-icon [icon]="faTrash"></fa-icon>
|
<fa-icon [icon]="faTrash"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -2,9 +2,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.plot {
|
||||||
width: 100%;
|
height: 60vw;
|
||||||
height: 40vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subSeries {
|
.subSeries {
|
||||||
147
src/main/angular/src/app/plot/editor/plot-editor.component.ts
Normal file
147
src/main/angular/src/app/plot/editor/plot-editor.component.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {BarController, BarElement, CategoryScale, Chart, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
||||||
|
import {SeriesService} from "../../series/series.service";
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import {SeriesType} from '../../series/SeriesType';
|
||||||
|
import {PlotService} from '../plot.service';
|
||||||
|
import {Plot} from '../Plot';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {faCopy, faEye, faListAlt} from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import {TextComponent} from '../../common/text/text.component';
|
||||||
|
import {NumberComponent} from '../../common/number/number.component';
|
||||||
|
import {CheckboxComponent} from '../../common/checkbox/checkbox.component';
|
||||||
|
import {Group} from '../Group';
|
||||||
|
import {NumberNNComponent} from '../../common/numberNN/number-n-n.component';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {Interval} from '../../series/Interval';
|
||||||
|
import {faChartLine, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {Location} from '@angular/common';
|
||||||
|
import {PlotComponent} from '../plot/plot.component';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
import {GraphType} from '../axis/graph/GraphType';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
TimeScale,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function unitInBrackets(unit: string): string {
|
||||||
|
if (!unit) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return ` [${unit}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-plot-editor',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
FaIconComponent,
|
||||||
|
TextComponent,
|
||||||
|
NumberComponent,
|
||||||
|
CheckboxComponent,
|
||||||
|
NumberNNComponent,
|
||||||
|
PlotComponent
|
||||||
|
],
|
||||||
|
templateUrl: './plot-editor.component.html',
|
||||||
|
styleUrl: './plot-editor.component.less'
|
||||||
|
})
|
||||||
|
export class PlotEditor implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly SeriesType = SeriesType;
|
||||||
|
|
||||||
|
protected readonly Interval = Interval;
|
||||||
|
|
||||||
|
protected readonly faEye = faEye;
|
||||||
|
|
||||||
|
protected readonly faListAlt = faListAlt;
|
||||||
|
|
||||||
|
protected readonly faCopy = faCopy;
|
||||||
|
|
||||||
|
protected readonly faPlus = faPlus;
|
||||||
|
|
||||||
|
protected readonly faTrash = faTrash;
|
||||||
|
|
||||||
|
protected readonly faChartLine = faChartLine;
|
||||||
|
|
||||||
|
private id: number = NaN;
|
||||||
|
|
||||||
|
private _plot: Plot | null = null;
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly activatedRoute: ActivatedRoute,
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
readonly plotService: PlotService,
|
||||||
|
readonly location: Location
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
get plot(): Plot | null {
|
||||||
|
return this._plot;
|
||||||
|
}
|
||||||
|
|
||||||
|
set plot(value: Plot | null) {
|
||||||
|
this._plot = value;
|
||||||
|
if (this._plot) {
|
||||||
|
this.id = this._plot.id;
|
||||||
|
}
|
||||||
|
if (isNaN(this.id)) {
|
||||||
|
this.location.go("/PlotEditor");
|
||||||
|
} else {
|
||||||
|
this.location.go("/PlotEditor/" + this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.activatedRoute.params.subscribe(params => {
|
||||||
|
this.id = parseInt(params['id']);
|
||||||
|
this.loadPlot();
|
||||||
|
this.subs.push(this.plotService.subscribeListItems(fresh => {
|
||||||
|
if (fresh.deleted) {
|
||||||
|
if (fresh.hasId(this.id)) {
|
||||||
|
this.id = NaN;
|
||||||
|
}
|
||||||
|
} else if (isNaN(this.id)) {
|
||||||
|
}
|
||||||
|
this.loadPlot();
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPlot() {
|
||||||
|
if (isNaN(this.id)) {
|
||||||
|
this.plot = this.plotService.list[0] || null;
|
||||||
|
this.id = this.plot?.id || NaN;
|
||||||
|
} else {
|
||||||
|
this.plot = this.plotService.byId(this.id) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(subscription => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly setPlot = (plot: Plot): void => {
|
||||||
|
this.plot = plot;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected groups(): string[] {
|
||||||
|
return Object.keys(Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly GraphType = GraphType;
|
||||||
|
}
|
||||||
@ -1,374 +0,0 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
|
||||||
import {BarController, BarElement, CategoryScale, Chart, ChartDataset, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
|
||||||
import {SeriesService} from "../series/series.service";
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import {toAvg, toBool, toDelta, toMax, toMin} from '../series/MinMaxAvg';
|
|
||||||
import {SeriesType} from '../series/SeriesType';
|
|
||||||
import {PlotService} from './plot.service';
|
|
||||||
import {Plot} from './Plot';
|
|
||||||
import {FormsModule} from '@angular/forms';
|
|
||||||
import {NTU, UTN} from '../COMMON';
|
|
||||||
import {Graph} from './Graph';
|
|
||||||
import {Axis} from './Axis';
|
|
||||||
import {Series} from '../series/Series';
|
|
||||||
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
|
||||||
import {faCopy, faEye, faListAlt} from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import {TextComponent} from '../common/text/text.component';
|
|
||||||
import {NumberComponent} from '../common/number/number.component';
|
|
||||||
import {CheckboxComponent} from '../common/checkbox/checkbox.component';
|
|
||||||
import {Group} from './Group';
|
|
||||||
import {NumberNNComponent} from '../common/numberNN/number-n-n.component';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {Delta, DeltaService} from '../series/delta/delta-service';
|
|
||||||
import {Bool, BoolService} from '../series/bool/bool-service';
|
|
||||||
import {Varying, VaryingService} from '../series/varying/varying-service';
|
|
||||||
import {Interval} from '../series/Interval';
|
|
||||||
import {faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import {ActivatedRoute} from '@angular/router';
|
|
||||||
import {Location} from '@angular/common';
|
|
||||||
|
|
||||||
type Dataset = ChartDataset<any, any>[][number];
|
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
BarController,
|
|
||||||
BarElement,
|
|
||||||
LineController,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
TimeScale,
|
|
||||||
Filler,
|
|
||||||
);
|
|
||||||
|
|
||||||
export function unitInBrackets(unit: string): string {
|
|
||||||
if (!unit) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return ` [${unit}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-plot',
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
FaIconComponent,
|
|
||||||
TextComponent,
|
|
||||||
NumberComponent,
|
|
||||||
CheckboxComponent,
|
|
||||||
NumberNNComponent
|
|
||||||
],
|
|
||||||
templateUrl: './plot.component.html',
|
|
||||||
styleUrl: './plot.component.less'
|
|
||||||
})
|
|
||||||
export class PlotComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
||||||
|
|
||||||
protected readonly SeriesType = SeriesType;
|
|
||||||
|
|
||||||
protected readonly Interval = Interval;
|
|
||||||
|
|
||||||
protected readonly faEye = faEye;
|
|
||||||
|
|
||||||
protected readonly faListAlt = faListAlt;
|
|
||||||
|
|
||||||
protected readonly faCopy = faCopy;
|
|
||||||
|
|
||||||
protected readonly faPlus = faPlus;
|
|
||||||
|
|
||||||
protected readonly faTrash = faTrash;
|
|
||||||
|
|
||||||
@ViewChild('chartCanvas')
|
|
||||||
canvasRef!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
@ViewChild('container')
|
|
||||||
chartContainer!: ElementRef<HTMLDivElement>;
|
|
||||||
|
|
||||||
private chart!: Chart;
|
|
||||||
|
|
||||||
protected seriesList: Series[] = [];
|
|
||||||
|
|
||||||
protected plotList: Plot[] = [];
|
|
||||||
|
|
||||||
protected plot: Plot | null = null;
|
|
||||||
|
|
||||||
private readonly subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
private readonly graphSubscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly serieService: SeriesService,
|
|
||||||
readonly plotService: PlotService,
|
|
||||||
readonly boolService: BoolService,
|
|
||||||
readonly deltaService: DeltaService,
|
|
||||||
readonly varyingService: VaryingService,
|
|
||||||
readonly activatedRoute: ActivatedRoute,
|
|
||||||
readonly location: Location
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
|
||||||
this.chart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
datasets: [],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
animation: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
position: 'nearest',
|
|
||||||
mode: 'x',
|
|
||||||
usePointStyle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.serieService.findAll(series => {
|
|
||||||
this.seriesList = series.sort(Series.compareName);
|
|
||||||
this.plotService.findAll(plots => {
|
|
||||||
this.plotList = plots.sort(Plot.comparePosition);
|
|
||||||
this.subscriptions.push(this.plotService.subscribe(this.updatePlot));
|
|
||||||
this.activatedRoute.params.subscribe(params => {
|
|
||||||
const id = parseInt(params["id"]);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
this.buildPlot(this.plotList[0]);
|
|
||||||
} else {
|
|
||||||
this.buildPlot(this.plotList.filter(plot => plot.id === id)[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.subscriptions.push(this.serieService.subscribe(this.updateSeries));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly buildPlot = (plot: Plot | null | undefined): void => {
|
|
||||||
this.plot = UTN(plot);
|
|
||||||
|
|
||||||
this.graphSubscriptions.forEach(s => s.unsubscribe());
|
|
||||||
this.graphSubscriptions.length = 0;
|
|
||||||
for (const key in this.chart.options.scales) {
|
|
||||||
if (key.startsWith("y")) {
|
|
||||||
delete this.chart.options.scales[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.chart.data.labels = [];
|
|
||||||
this.chart.data.datasets = [];
|
|
||||||
this.chart.update();
|
|
||||||
|
|
||||||
if (!this.plot) {
|
|
||||||
this.location.go('/plot');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.location.go('/plot/' + this.plot.id);
|
|
||||||
if (!this.chart.options.plugins) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.chart.options.plugins.title = {
|
|
||||||
display: !!this.plot.name,
|
|
||||||
text: this.plot.name,
|
|
||||||
};
|
|
||||||
for (const axis of this.plot.axes) {
|
|
||||||
this.buildAxis(axis);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private buildAxis(axis: Axis): void {
|
|
||||||
if (!this.chart.options.scales) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = axis.name + unitInBrackets(axis.unit);
|
|
||||||
this.chart.options.scales["y" + axis.id] = {
|
|
||||||
display: axis.visible,
|
|
||||||
title: {
|
|
||||||
display: !!name,
|
|
||||||
text: name,
|
|
||||||
},
|
|
||||||
suggestedMin: NTU(axis.min),
|
|
||||||
suggestedMax: NTU(axis.max),
|
|
||||||
min: axis.minHard ? NTU(axis.min) : undefined,
|
|
||||||
max: axis.maxHard ? NTU(axis.max) : undefined,
|
|
||||||
position: axis.right ? 'right' : 'left',
|
|
||||||
};
|
|
||||||
for (const graph of axis.graphs) {
|
|
||||||
this.buildGraph(graph);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildGraph(graph: Graph): void {
|
|
||||||
if (!graph.visible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const midSuffix = graph.series.type === SeriesType.DELTA || graph.series.type === SeriesType.BOOL ? "" : " Ø"
|
|
||||||
const min: Dataset = graph.min ? this.newDataset(graph, " min", false) : null;
|
|
||||||
const mid: Dataset = graph.mid ? this.newDataset(graph, midSuffix, min ? '-1' : graph.series.type === SeriesType.BOOL) : null;
|
|
||||||
const max: Dataset = graph.max ? this.newDataset(graph, " max", min || mid ? '-1' : false) : null;
|
|
||||||
switch (graph.series.type) {
|
|
||||||
case SeriesType.BOOL:
|
|
||||||
this.graphSubscriptions.push(this.boolService.subscribe(bool => this.updateBool(bool, mid), [graph.series.id]));
|
|
||||||
break;
|
|
||||||
case SeriesType.DELTA:
|
|
||||||
this.graphSubscriptions.push(this.deltaService.subscribe(delta => this.updateDelta(delta, mid), [graph.series.id, graph.axis.plot.interval.name]));
|
|
||||||
break;
|
|
||||||
case SeriesType.VARYING:
|
|
||||||
this.graphSubscriptions.push(this.varyingService.subscribe(varying => this.updateVarying(varying, min, mid, max), [graph.series.id, graph.axis.plot.interval.name]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.points(graph, min, mid, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
private newDataset(graph: Graph, suffix: string, fill: string | number | boolean): Dataset {
|
|
||||||
this.chart.data.datasets.push({
|
|
||||||
data: [],
|
|
||||||
label: `${graph.name || graph.series.name}${suffix}${unitInBrackets(graph.series.unit)}`,
|
|
||||||
type: graph.type as "line", // TODO
|
|
||||||
fill: fill || (graph.stack ? 'stack' : false),
|
|
||||||
stack: graph.stack || undefined,
|
|
||||||
borderWidth: 1,
|
|
||||||
// barThickness: 'flex', // TODO
|
|
||||||
borderColor: graph.color,
|
|
||||||
pointRadius: graph.series.type === SeriesType.BOOL ? 0 : 4,
|
|
||||||
pointBackgroundColor: graph.color,
|
|
||||||
backgroundColor: graph.color + "33",
|
|
||||||
yAxisID: "y" + graph.axis.id,
|
|
||||||
pointStyle: graph.type === 'bar' || graph.series.type === SeriesType.BOOL ? 'rect' : 'crossRot',
|
|
||||||
spanGaps: graph.series.type === SeriesType.BOOL ? Infinity : graph.axis.plot.interval.spanGaps,
|
|
||||||
});
|
|
||||||
return this.chart.data.datasets[this.chart.data.datasets.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void {
|
|
||||||
this.serieService.points(
|
|
||||||
graph.series,
|
|
||||||
graph.axis.plot.interval,
|
|
||||||
graph.axis.plot.offset,
|
|
||||||
graph.axis.plot.duration,
|
|
||||||
points => {
|
|
||||||
if (graph.series.type === SeriesType.BOOL) {
|
|
||||||
mid.data = toBool(points, graph.factor);
|
|
||||||
} else if (graph.series.type === SeriesType.DELTA) {
|
|
||||||
mid.data = toDelta(points, graph.factor);
|
|
||||||
} else if (graph.series.type === SeriesType.VARYING) {
|
|
||||||
if (min) {
|
|
||||||
min.data = toMin(points, graph.factor);
|
|
||||||
}
|
|
||||||
if (max) {
|
|
||||||
max.data = toMax(points, graph.factor);
|
|
||||||
}
|
|
||||||
if (mid) {
|
|
||||||
mid.data = toAvg(points, graph.factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.chart.update();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly updatePlot = (fresh: Plot): void => {
|
|
||||||
const index = this.plotList.findIndex(plot => plot.id === fresh.id);
|
|
||||||
if (fresh.deleted) {
|
|
||||||
if (index >= 0) {
|
|
||||||
this.plotList.splice(index, 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (index < 0) {
|
|
||||||
this.plotList.push(fresh);
|
|
||||||
} else {
|
|
||||||
this.plotList[index] = fresh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.plotList = this.plotList.sort(Plot.comparePosition);
|
|
||||||
|
|
||||||
const selected = this.plot?.id === fresh.id;
|
|
||||||
if (fresh.deleted) {
|
|
||||||
if (selected) {
|
|
||||||
const previousIndex = Math.max(0, index - 1);
|
|
||||||
const previousPlot = this.plotList[previousIndex];
|
|
||||||
this.buildPlot(previousPlot);
|
|
||||||
}
|
|
||||||
} else if (!this.plot || selected) {
|
|
||||||
this.buildPlot(fresh);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected readonly updateSeries = (fresh: Series): void => {
|
|
||||||
const index = this.seriesList.findIndex(series => series.id === fresh.id);
|
|
||||||
if (index < 0) {
|
|
||||||
this.seriesList.push(fresh);
|
|
||||||
} else if (fresh.deleted) {
|
|
||||||
this.seriesList.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
this.seriesList[index] = fresh;
|
|
||||||
}
|
|
||||||
this.seriesList = this.seriesList.sort(Series.compareName);
|
|
||||||
};
|
|
||||||
|
|
||||||
protected groups(): string[] {
|
|
||||||
return Object.keys(Group);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateBool(bool: Bool, mid: Dataset): void {
|
|
||||||
this.updatePoint(mid, bool.date, bool.state ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateDelta(delta: Delta, mid: Dataset): void {
|
|
||||||
this.updatePoint(mid, delta.date, delta.last - delta.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateVarying(varying: Varying, min: Dataset | null, avg: Dataset | null, max: Dataset | null): void {
|
|
||||||
if (min) {
|
|
||||||
this.updatePoint(min, varying.date, varying.min);
|
|
||||||
}
|
|
||||||
if (avg) {
|
|
||||||
this.updatePoint(avg, varying.date, varying.avg);
|
|
||||||
}
|
|
||||||
if (max) {
|
|
||||||
this.updatePoint(max, varying.date, varying.max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updatePoint(dataset: Dataset, date: Date, y: number): void {
|
|
||||||
const x = date.getTime();
|
|
||||||
const point = dataset.data.filter((p: PointElement) => p.x === x)[0];
|
|
||||||
if (point) {
|
|
||||||
if (point.y !== y) {
|
|
||||||
point.y = y;
|
|
||||||
this.chart.update();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updateAndBuild = (plot: Plot): void => {
|
|
||||||
this.updatePlot(plot);
|
|
||||||
this.buildPlot(plot);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,35 +1,36 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ApiService, CrudService, Next} from '../COMMON';
|
import {ApiService, EntityListService, Next} from '../COMMON';
|
||||||
import {Plot} from './Plot';
|
import {Plot} from './Plot';
|
||||||
import {Axis} from './Axis';
|
import {Axis} from './axis/Axis';
|
||||||
import {Graph} from './Graph';
|
import {Graph} 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';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class PlotService extends CrudService<Plot> {
|
export class PlotService extends EntityListService<Plot> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiService,
|
api: ApiService,
|
||||||
) {
|
) {
|
||||||
super(api, ["Plot"], Plot.fromJson);
|
super(api, ["Plot"], Plot.fromJson, Plot.equals, Plot.comparePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
plotCreate(next: Next<Plot>): void {
|
plotCreate(next?: Next<Plot>): void {
|
||||||
this.getSingle(["create"], next);
|
this.getSingle(["create"], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
plotDuplicate(plot: Plot, next: Next<Plot>): void {
|
plotDuplicate(plot: Plot, next?: Next<Plot>): void {
|
||||||
this.getSingle([plot.id, "duplicate"], next);
|
this.getSingle([plot.id, "duplicate"], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
plotDelete(plot: Plot, next: Next<Plot>): void {
|
plotDelete(plot: Plot, next?: Next<Plot>): void {
|
||||||
this.getSingle([plot.id, "delete"], next);
|
this.getSingle([plot.id, "delete"], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
plotAddAxis(plot: Plot, next: Next<Plot>): void {
|
plotAddAxis(plot: Plot, next?: Next<Plot>): void {
|
||||||
this.getSingle([plot.id, 'addAxis'], next);
|
this.getSingle([plot.id, 'addAxis'], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ export class PlotService extends CrudService<Plot> {
|
|||||||
this.postSingle([plot.id, 'position'], value, next);
|
this.postSingle([plot.id, 'position'], value, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
axisAddGraph(axis: Axis, next: Next<Plot>): void {
|
axisAddGraph(axis: Axis, next?: Next<Plot>): void {
|
||||||
this.getSingle(['Axis', axis.id, 'addGraph'], next);
|
this.getSingle(['Axis', axis.id, 'addGraph'], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +106,10 @@ export class PlotService extends CrudService<Plot> {
|
|||||||
this.postSingle(['Graph', graph.id, 'visible'], value, next);
|
this.postSingle(['Graph', graph.id, 'visible'], value, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
graphType(graph: Graph, value: GraphType, next?: Next<Plot>): void {
|
||||||
|
this.postSingle(['Graph', graph.id, 'type'], value.jsonName, next);
|
||||||
|
}
|
||||||
|
|
||||||
graphName(graph: Graph, value: string, next?: Next<Plot>): void {
|
graphName(graph: Graph, value: string, next?: Next<Plot>): void {
|
||||||
this.postSingle(['Graph', graph.id, 'name'], value, next);
|
this.postSingle(['Graph', graph.id, 'name'], value, next);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/main/angular/src/app/plot/plot/plot.component.html
Normal file
3
src/main/angular/src/app/plot/plot/plot.component.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div #container class="container">
|
||||||
|
<canvas #chartCanvas></canvas>
|
||||||
|
</div>
|
||||||
4
src/main/angular/src/app/plot/plot/plot.component.less
Normal file
4
src/main/angular/src/app/plot/plot/plot.component.less
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
277
src/main/angular/src/app/plot/plot/plot.component.ts
Normal file
277
src/main/angular/src/app/plot/plot/plot.component.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild} from '@angular/core';
|
||||||
|
import {BarController, BarElement, CategoryScale, Chart, ChartDataset, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import {toAvg, toBool, toDelta, toMax, toMin} from '../../series/MinMaxAvg';
|
||||||
|
import {SeriesType} from '../../series/SeriesType';
|
||||||
|
import {Plot} from '../Plot';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {NTU, UTN} from '../../COMMON';
|
||||||
|
import {Graph} from '../axis/graph/Graph';
|
||||||
|
import {Axis} from '../axis/Axis';
|
||||||
|
import {Subscription} from 'rxjs';
|
||||||
|
import {Delta, DeltaService} from '../../series/delta/delta-service';
|
||||||
|
import {Bool, BoolService} from '../../series/bool/bool-service';
|
||||||
|
import {Varying, VaryingService} from '../../series/varying/varying-service';
|
||||||
|
import {Interval} from '../../series/Interval';
|
||||||
|
import {SeriesService} from '../../series/series.service';
|
||||||
|
import {GraphType} from '../axis/graph/GraphType';
|
||||||
|
|
||||||
|
type Dataset = ChartDataset<any, any>[][number];
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
TimeScale,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function unitInBrackets(unit: string): string {
|
||||||
|
if (!unit) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return ` [${unit}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-plot',
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './plot.component.html',
|
||||||
|
styleUrl: './plot.component.less'
|
||||||
|
})
|
||||||
|
export class PlotComponent implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly SeriesType = SeriesType;
|
||||||
|
|
||||||
|
protected readonly Interval = Interval;
|
||||||
|
|
||||||
|
@ViewChild('chartCanvas')
|
||||||
|
protected canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
@ViewChild('container')
|
||||||
|
protected chartContainer!: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
private chart!: Chart;
|
||||||
|
|
||||||
|
protected _plot: Plot | null = null;
|
||||||
|
|
||||||
|
get plot(): Plot | null {
|
||||||
|
return this._plot;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly seriesService: SeriesService,
|
||||||
|
readonly boolService: BoolService,
|
||||||
|
readonly deltaService: DeltaService,
|
||||||
|
readonly varyingService: VaryingService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(subscription => subscription.unsubscribe());
|
||||||
|
this.chart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: [],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
position: 'nearest',
|
||||||
|
mode: 'x',
|
||||||
|
usePointStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.plot = this.plot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set plot(plot: Plot | null) {
|
||||||
|
this._plot = UTN(plot);
|
||||||
|
|
||||||
|
this.subs.forEach(s => s.unsubscribe());
|
||||||
|
this.subs.length = 0;
|
||||||
|
if (!this.chart?.options?.plugins) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in this.chart.options.scales) {
|
||||||
|
if (key.startsWith("y")) {
|
||||||
|
delete this.chart.options.scales[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.chart.data.labels = [];
|
||||||
|
this.chart.data.datasets = [];
|
||||||
|
|
||||||
|
this.chart.update();
|
||||||
|
if (!this.plot) {
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
this.chart.options.plugins.title = {
|
||||||
|
display: !!this.plot.name,
|
||||||
|
text: this.plot.name,
|
||||||
|
};
|
||||||
|
for (const axis of this.plot.axes) {
|
||||||
|
this.buildAxis(axis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private buildAxis(axis: Axis): void {
|
||||||
|
if (!this.chart.options.scales) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = axis.name + unitInBrackets(axis.unit);
|
||||||
|
this.chart.options.scales["y" + axis.id] = {
|
||||||
|
display: axis.visible,
|
||||||
|
title: {
|
||||||
|
display: !!name,
|
||||||
|
text: name,
|
||||||
|
},
|
||||||
|
suggestedMin: NTU(axis.min),
|
||||||
|
suggestedMax: NTU(axis.max),
|
||||||
|
min: axis.minHard ? NTU(axis.min) : undefined,
|
||||||
|
max: axis.maxHard ? NTU(axis.max) : undefined,
|
||||||
|
position: axis.right ? 'right' : 'left',
|
||||||
|
};
|
||||||
|
for (const graph of axis.graphs) {
|
||||||
|
this.buildGraph(graph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGraph(graph: Graph): void {
|
||||||
|
if (!graph.visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const midSuffix = graph.series.type === SeriesType.DELTA || graph.series.type === SeriesType.BOOL ? "" : " Ø"
|
||||||
|
const min: Dataset = graph.showMin ? this.newDataset(graph, " min", false) : null;
|
||||||
|
const mid: Dataset = graph.showMid ? this.newDataset(graph, midSuffix, min ? '-1' : graph.series.type === SeriesType.BOOL) : null;
|
||||||
|
const max: Dataset = graph.showMax ? this.newDataset(graph, " max", min || mid ? '-1' : false) : null;
|
||||||
|
switch (graph.series.type) {
|
||||||
|
case SeriesType.BOOL:
|
||||||
|
this.subs.push(this.boolService.subscribe(bool => this.updateBool(graph, bool, mid), [graph.series.id]));
|
||||||
|
break;
|
||||||
|
case SeriesType.DELTA:
|
||||||
|
this.subs.push(this.deltaService.subscribe(delta => this.updateDelta(graph, delta, mid), [graph.series.id, graph.axis.plot.interval.name]));
|
||||||
|
break;
|
||||||
|
case SeriesType.VARYING:
|
||||||
|
this.subs.push(this.varyingService.subscribe(varying => this.updateVarying(graph, varying, min, mid, max), [graph.series.id, graph.axis.plot.interval.name]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.points(graph, min, mid, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private newDataset(graph: Graph, suffix: string, fill: string | number | boolean): Dataset {
|
||||||
|
this.chart.data.datasets.push({
|
||||||
|
data: [],
|
||||||
|
label: `${graph.name || graph.series.name}${suffix}${unitInBrackets(graph.series.unit)}`,
|
||||||
|
type: graph.type.chartJsName,
|
||||||
|
fill: fill || (graph.stack ? 'stack' : false),
|
||||||
|
stack: graph.stack || undefined,
|
||||||
|
borderWidth: 1,
|
||||||
|
barThickness: 'flex',
|
||||||
|
borderColor: graph.color,
|
||||||
|
pointRadius: graph.series.type === SeriesType.BOOL ? 0 : 4,
|
||||||
|
pointBackgroundColor: graph.color,
|
||||||
|
backgroundColor: graph.color + "33",
|
||||||
|
yAxisID: "y" + graph.axis.id,
|
||||||
|
pointStyle: graph.type === GraphType.BAR || graph.series.type === SeriesType.BOOL ? 'rect' : 'crossRot',
|
||||||
|
spanGaps: graph.series.type === SeriesType.BOOL ? Infinity : graph.axis.plot.interval.spanGaps,
|
||||||
|
});
|
||||||
|
return this.chart.data.datasets[this.chart.data.datasets.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void {
|
||||||
|
this.seriesService.points(
|
||||||
|
graph.series,
|
||||||
|
graph.axis.plot.interval,
|
||||||
|
graph.axis.plot.offset,
|
||||||
|
graph.axis.plot.duration,
|
||||||
|
points => {
|
||||||
|
if (graph.series.type === SeriesType.BOOL) {
|
||||||
|
mid.data = toBool(points, graph.factor);
|
||||||
|
} else if (graph.series.type === SeriesType.DELTA) {
|
||||||
|
mid.data = toDelta(points, graph.factor);
|
||||||
|
} else if (graph.series.type === SeriesType.VARYING) {
|
||||||
|
if (min) {
|
||||||
|
min.data = toMin(points, graph.factor);
|
||||||
|
}
|
||||||
|
if (max) {
|
||||||
|
max.data = toMax(points, graph.factor);
|
||||||
|
}
|
||||||
|
if (mid) {
|
||||||
|
mid.data = toAvg(points, graph.factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.chart.update();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBool(graph: Graph, bool: Bool, mid: Dataset): void {
|
||||||
|
this.updatePoint(graph, mid, bool.date, bool.state ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateDelta(graph: Graph, delta: Delta, mid: Dataset): void {
|
||||||
|
this.updatePoint(graph, mid, delta.date, delta.last - delta.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVarying(graph: Graph, varying: Varying, min: Dataset | null, avg: Dataset | null, max: Dataset | null): void {
|
||||||
|
if (min) {
|
||||||
|
this.updatePoint(graph, min, varying.date, varying.min);
|
||||||
|
}
|
||||||
|
if (avg) {
|
||||||
|
this.updatePoint(graph, avg, varying.date, varying.avg);
|
||||||
|
}
|
||||||
|
if (max) {
|
||||||
|
this.updatePoint(graph, max, varying.date, varying.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePoint(graph: Graph, dataset: Dataset, date: Date, y: number): void {
|
||||||
|
const x = date.getTime();
|
||||||
|
const point = dataset.data.filter((p: PointElement) => p.x === x)[0];
|
||||||
|
const yMultiplied = y * graph.factor;
|
||||||
|
if (point) {
|
||||||
|
if (point.y !== yMultiplied) {
|
||||||
|
point.y = yMultiplied;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataset.data.push({x: x, y: yMultiplied}); // TODO check if this is a LIVE/SCROLLING plot (right end of plot is 'now')
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import {mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON";
|
import {ID, mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON";
|
||||||
import {SeriesType} from './SeriesType';
|
import {SeriesType} from './SeriesType';
|
||||||
import {formatNumber} from '@angular/common';
|
import {formatNumber} from '@angular/common';
|
||||||
|
|
||||||
export class Series {
|
export class Series extends ID<Series> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: number,
|
readonly id: number,
|
||||||
@ -14,7 +14,7 @@ export class Series {
|
|||||||
readonly decimals: number,
|
readonly decimals: number,
|
||||||
readonly value: number | null,
|
readonly value: number | null,
|
||||||
) {
|
) {
|
||||||
//
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: any): Series {
|
static fromJson(json: any): Series {
|
||||||
@ -46,4 +46,7 @@ export class Series {
|
|||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equals2() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ApiService, CrudService, Next, validateNumber} from "../COMMON";
|
import {ApiService, EntityListService, Next, validateNumber} from "../COMMON";
|
||||||
import {Series} from './Series';
|
import {Series} from './Series';
|
||||||
import {Interval} from './Interval';
|
import {Interval} from './Interval';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class SeriesService extends CrudService<Series> {
|
export class SeriesService extends EntityListService<Series> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiService,
|
api: ApiService,
|
||||||
) {
|
) {
|
||||||
super(api, ['Series'], Series.fromJson);
|
super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
points(series: Series, interval: Interval, offset: number, duration: number, next: Next<number[][]>): void {
|
points(series: Series, interval: Interval, offset: number, duration: number, next: Next<number[][]>): void {
|
||||||
|
|||||||
@ -3,6 +3,10 @@ html, body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -27,3 +31,22 @@ input, select {
|
|||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
padding: 0.125em;
|
padding: 0.125em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
box-shadow: 0 0 0.25em black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonAdd {
|
||||||
|
background-color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonCopy {
|
||||||
|
background-color: lightskyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonRemove {
|
||||||
|
background-color: indianred;
|
||||||
|
}
|
||||||
|
|||||||
@ -50,6 +50,17 @@ public class Graph {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean visible = true;
|
private boolean visible = true;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private GraphType type = GraphType.LINE;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String fill = "";
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@NonNull
|
@NonNull
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@ -41,6 +41,11 @@ public class GraphController {
|
|||||||
return graphService.set(id, graph -> graph.setVisible(value));
|
return graphService.set(id, graph -> graph.setVisible(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("{id}/type")
|
||||||
|
public PlotDto type(@PathVariable final long id, @RequestBody @NonNull final String value) {
|
||||||
|
return graphService.set(id, graph -> graph.setType(GraphType.valueOf(value)));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("{id}/name")
|
@PostMapping("{id}/name")
|
||||||
public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) {
|
||||||
return graphService.set(id, graph -> graph.setName(or(value, "")));
|
return graphService.set(id, graph -> graph.setName(or(value, "")));
|
||||||
|
|||||||
@ -19,6 +19,10 @@ public class GraphDto {
|
|||||||
|
|
||||||
public final boolean visible;
|
public final boolean visible;
|
||||||
|
|
||||||
|
public final GraphType type;
|
||||||
|
|
||||||
|
public final String fill;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public final String color;
|
public final String color;
|
||||||
|
|
||||||
@ -42,6 +46,8 @@ public class GraphDto {
|
|||||||
this.series = new SeriesDto(graph.getSeries(), false);
|
this.series = new SeriesDto(graph.getSeries(), false);
|
||||||
this.name = graph.getName();
|
this.name = graph.getName();
|
||||||
this.visible = graph.isVisible();
|
this.visible = graph.isVisible();
|
||||||
|
this.type = graph.getType();
|
||||||
|
this.fill = graph.getFill();
|
||||||
this.color = graph.getColor();
|
this.color = graph.getColor();
|
||||||
this.factor = graph.getFactor();
|
this.factor = graph.getFactor();
|
||||||
this.group = graph.getGroup();
|
this.group = graph.getGroup();
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.data.plot.axis.graph;
|
||||||
|
|
||||||
|
public enum GraphType {
|
||||||
|
LINE, BAR
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user