From 6172e888bf30414f901243bb32dc0d6b435c55de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Mon, 22 Sep 2025 15:40:04 +0200 Subject: [PATCH] extracted Plot into own component + Dashboard + Graph.type + Graph.fill --- src/main/angular/src/app/COMMON.ts | 83 +++- src/main/angular/src/app/app.html | 16 + src/main/angular/src/app/app.less | 51 +++ src/main/angular/src/app/app.routes.ts | 10 +- src/main/angular/src/app/app.ts | 10 +- .../app/dashboard/dashboard.component.html | 5 + .../app/dashboard/dashboard.component.less | 4 + .../src/app/dashboard/dashboard.component.ts | 26 ++ src/main/angular/src/app/plot/Plot.ts | 7 +- .../angular/src/app/plot/{ => axis}/Axis.ts | 6 +- .../src/app/plot/{ => axis/graph}/Graph.ts | 25 +- .../src/app/plot/axis/graph/GraphType.ts | 32 ++ .../plot-editor.component.html} | 86 ++-- .../plot-editor.component.less} | 6 +- .../app/plot/editor/plot-editor.component.ts | 147 +++++++ .../angular/src/app/plot/plot.component.ts | 374 ------------------ src/main/angular/src/app/plot/plot.service.ts | 25 +- .../src/app/plot/plot/plot.component.html | 3 + .../src/app/plot/plot/plot.component.less | 4 + .../src/app/plot/plot/plot.component.ts | 277 +++++++++++++ src/main/angular/src/app/series/Series.ts | 9 +- .../angular/src/app/series/series.service.ts | 6 +- src/main/angular/src/styles.less | 23 ++ .../de/ph87/data/plot/axis/graph/Graph.java | 11 + .../data/plot/axis/graph/GraphController.java | 5 + .../ph87/data/plot/axis/graph/GraphDto.java | 6 + .../ph87/data/plot/axis/graph/GraphType.java | 5 + 27 files changed, 806 insertions(+), 456 deletions(-) create mode 100644 src/main/angular/src/app/dashboard/dashboard.component.html create mode 100644 src/main/angular/src/app/dashboard/dashboard.component.less create mode 100644 src/main/angular/src/app/dashboard/dashboard.component.ts rename src/main/angular/src/app/plot/{ => axis}/Axis.ts (91%) rename src/main/angular/src/app/plot/{ => axis/graph}/Graph.ts (58%) create mode 100644 src/main/angular/src/app/plot/axis/graph/GraphType.ts rename src/main/angular/src/app/plot/{plot.component.html => editor/plot-editor.component.html} (69%) rename src/main/angular/src/app/plot/{plot.component.less => editor/plot-editor.component.less} (89%) create mode 100644 src/main/angular/src/app/plot/editor/plot-editor.component.ts delete mode 100644 src/main/angular/src/app/plot/plot.component.ts create mode 100644 src/main/angular/src/app/plot/plot/plot.component.html create mode 100644 src/main/angular/src/app/plot/plot/plot.component.less create mode 100644 src/main/angular/src/app/plot/plot/plot.component.ts create mode 100644 src/main/java/de/ph87/data/plot/axis/graph/GraphType.java diff --git a/src/main/angular/src/app/COMMON.ts b/src/main/angular/src/app/COMMON.ts index e52a064..bc2a7fd 100644 --- a/src/main/angular/src/app/COMMON.ts +++ b/src/main/angular/src/app/COMMON.ts @@ -1,16 +1,19 @@ 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 {Injectable} from '@angular/core'; import {RxStompState} from '@stomp/rx-stomp'; export type FromJson = (json: any) => T; + export type FromJsonIndexed = (json: any, index: number) => T; export type Next = (item: T) => any; export type Compare = (a: T, b: T) => number; +export type Equals = (a: T, b: T) => boolean; + export function stompServiceFactory() { const stomp = new StompService({ url: url('ws', ['websocket']), @@ -194,6 +197,84 @@ export abstract class CrudService { } +export abstract class ID> { + + 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>(a: X, b: X): boolean { + return ID.hasId(a.id)(b); + } + + static hasId>(id: number): (t: X) => boolean { + return (t: X) => t.hasId(id); + } + +} + +export abstract class EntityListService> extends CrudService { + + private readonly subject: Subject = new Subject(); + + private _list: T[] = []; + + protected constructor( + api: ApiService, + path: any[], + fromJson: FromJson, + readonly equals: Equals, + readonly compare: Compare, + ) { + 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): Subscription { + return this.subject.subscribe(next); + } + + byId(id: number): T { + return this._list.filter(t => t.hasId(id))[0]; + } + +} + export function NTU(v: T | null | undefined): T | undefined { if (v === null) { return undefined; diff --git a/src/main/angular/src/app/app.html b/src/main/angular/src/app/app.html index 7dd570e..cd9cf18 100644 --- a/src/main/angular/src/app/app.html +++ b/src/main/angular/src/app/app.html @@ -1 +1,17 @@ + + diff --git a/src/main/angular/src/app/app.less b/src/main/angular/src/app/app.less index e69de29..3fd6624 100644 --- a/src/main/angular/src/app/app.less +++ b/src/main/angular/src/app/app.less @@ -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; + } + + } + +} diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index 07df427..c541937 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -1,8 +1,10 @@ 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 = [ - {path: 'plot', component: PlotComponent}, - {path: 'plot/:id', component: PlotComponent}, - {path: '**', redirectTo: 'plot'}, + {path: 'Dashboard', component: DashboardComponent}, + {path: 'PlotEditor', component: PlotEditor}, + {path: 'PlotEditor/:id', component: PlotEditor}, + {path: '**', redirectTo: 'Dashboard'}, ]; diff --git a/src/main/angular/src/app/app.ts b/src/main/angular/src/app/app.ts index 9b04721..48224af 100644 --- a/src/main/angular/src/app/app.ts +++ b/src/main/angular/src/app/app.ts @@ -1,12 +1,14 @@ -import {Component, signal} from '@angular/core'; -import {RouterOutlet} from '@angular/router'; +import {Component} from '@angular/core'; +import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, RouterLink, RouterLinkActive], templateUrl: './app.html', styleUrl: './app.less' }) export class App { - protected readonly title = signal('angular'); + + protected sidebar: boolean = false; + } diff --git a/src/main/angular/src/app/dashboard/dashboard.component.html b/src/main/angular/src/app/dashboard/dashboard.component.html new file mode 100644 index 0000000..a3f3c7f --- /dev/null +++ b/src/main/angular/src/app/dashboard/dashboard.component.html @@ -0,0 +1,5 @@ +@for (plot of plots; track plot.id) { +
+ +
+} diff --git a/src/main/angular/src/app/dashboard/dashboard.component.less b/src/main/angular/src/app/dashboard/dashboard.component.less new file mode 100644 index 0000000..7e7c7c4 --- /dev/null +++ b/src/main/angular/src/app/dashboard/dashboard.component.less @@ -0,0 +1,4 @@ +.plot { + height: 60vw; + max-height: 100vh; +} diff --git a/src/main/angular/src/app/dashboard/dashboard.component.ts b/src/main/angular/src/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000..dced2b7 --- /dev/null +++ b/src/main/angular/src/app/dashboard/dashboard.component.ts @@ -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); + } + +} diff --git a/src/main/angular/src/app/plot/Plot.ts b/src/main/angular/src/app/plot/Plot.ts index 33ac00a..bf22884 100644 --- a/src/main/angular/src/app/plot/Plot.ts +++ b/src/main/angular/src/app/plot/Plot.ts @@ -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 {Axis} from './Axis'; +import {Axis} from './axis/Axis'; -export class Plot { +export class Plot extends ID { readonly axes: Axis[]; @@ -18,6 +18,7 @@ export class Plot { readonly position: number, axes: any[], ) { + super(); this.axes = validateListIndexed(axes, ((json, index) => Axis.fromJson(this, index, json))); } diff --git a/src/main/angular/src/app/plot/Axis.ts b/src/main/angular/src/app/plot/axis/Axis.ts similarity index 91% rename from src/main/angular/src/app/plot/Axis.ts rename to src/main/angular/src/app/plot/axis/Axis.ts index 6f5e247..cffd48f 100644 --- a/src/main/angular/src/app/plot/Axis.ts +++ b/src/main/angular/src/app/plot/axis/Axis.ts @@ -1,6 +1,6 @@ -import {mapNotNull, validateBoolean, validateList, validateNumber, validateString} from "../COMMON"; -import {Graph} from './Graph'; -import {Plot} from './Plot'; +import {mapNotNull, validateBoolean, validateList, validateNumber, validateString} from "../../COMMON"; +import {Graph} from './graph/Graph'; +import {Plot} from '../Plot'; export class Axis { diff --git a/src/main/angular/src/app/plot/Graph.ts b/src/main/angular/src/app/plot/axis/graph/Graph.ts similarity index 58% rename from src/main/angular/src/app/plot/Graph.ts rename to src/main/angular/src/app/plot/axis/graph/Graph.ts index 1d2242c..aa5fd53 100644 --- a/src/main/angular/src/app/plot/Graph.ts +++ b/src/main/angular/src/app/plot/axis/graph/Graph.ts @@ -1,14 +1,17 @@ -import {Series} from "../series/Series"; -import {Group} from "./Group"; -import {validateBoolean, validateNumber, validateString} from "../COMMON"; -import {Axis} from './Axis'; -import {SeriesType} from '../series/SeriesType'; +import {Series} from "../../../series/Series"; +import {Group} from "../../Group"; +import {validateBoolean, validateNumber, validateString} from "../../../COMMON"; +import {Axis} from '../Axis'; +import {SeriesType} from '../../../series/SeriesType'; +import {GraphType} from './GraphType'; export class Graph { - readonly type: string = "line"; + readonly showMin: boolean; - readonly mid: boolean; + readonly showMid: boolean; + + readonly showMax: boolean; constructor( readonly axis: Axis, @@ -17,6 +20,8 @@ export class Graph { readonly series: Series, readonly name: string, readonly visible: boolean, + readonly type: GraphType, + readonly fill: string, readonly color: string, readonly factor: number, readonly group: Group, @@ -25,7 +30,9 @@ export class Graph { readonly max: 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 { @@ -36,6 +43,8 @@ export class Graph { Series.fromJson(json.series), validateString(json.name), validateBoolean(json.visible), + GraphType.fromJson(json.type), + validateString(json.fill), validateString(json.color), validateNumber(json.factor), validateString(json.group) as Group, diff --git a/src/main/angular/src/app/plot/axis/graph/GraphType.ts b/src/main/angular/src/app/plot/axis/graph/GraphType.ts new file mode 100644 index 0000000..9805b50 --- /dev/null +++ b/src/main/angular/src/app/plot/axis/graph/GraphType.ts @@ -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; + } + +} diff --git a/src/main/angular/src/app/plot/plot.component.html b/src/main/angular/src/app/plot/editor/plot-editor.component.html similarity index 69% rename from src/main/angular/src/app/plot/plot.component.html rename to src/main/angular/src/app/plot/editor/plot-editor.component.html index 3bca068..c8c208a 100644 --- a/src/main/angular/src/app/plot/plot.component.html +++ b/src/main/angular/src/app/plot/editor/plot-editor.component.html @@ -1,23 +1,23 @@
- + @for (p of plotService.list; track p.id) { } - - -
-
- +
+
@if (plot) { @@ -34,26 +34,26 @@ - + - @for (interval of Interval.values; track interval) { } - + - + - + - + @@ -81,48 +81,46 @@ @for (axis of plot.axes; track axis.id) { - + Y{{ axis.index + 1 }} - + - + - + - + - + - + - + -   - } - @@ -138,6 +136,7 @@ Name Serie + Typ Farbe Faktor Aggregat @@ -152,43 +151,50 @@ @for (graph of axis.graphs; track graph.id) { - + - + - + @for (s of seriesService.list; track s.id) { } - + - + - @for (group of groups(); track group) { } - + @if (graph.series.type === SeriesType.VARYING) { - + - + - + } @else { @if (graph.series.type === SeriesType.BOOL) { @@ -198,14 +204,14 @@ } } - @for (axis of plot.axes; track axis.id) { } - diff --git a/src/main/angular/src/app/plot/plot.component.less b/src/main/angular/src/app/plot/editor/plot-editor.component.less similarity index 89% rename from src/main/angular/src/app/plot/plot.component.less rename to src/main/angular/src/app/plot/editor/plot-editor.component.less index a3609fa..b518dc4 100644 --- a/src/main/angular/src/app/plot/plot.component.less +++ b/src/main/angular/src/app/plot/editor/plot-editor.component.less @@ -2,9 +2,9 @@ display: flex; } -.container { - width: 100%; - height: 40vh; +.plot { + height: 60vw; + max-height: 50vh; } .subSeries { diff --git a/src/main/angular/src/app/plot/editor/plot-editor.component.ts b/src/main/angular/src/app/plot/editor/plot-editor.component.ts new file mode 100644 index 0000000..da7dd75 --- /dev/null +++ b/src/main/angular/src/app/plot/editor/plot-editor.component.ts @@ -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; +} diff --git a/src/main/angular/src/app/plot/plot.component.ts b/src/main/angular/src/app/plot/plot.component.ts deleted file mode 100644 index e18139e..0000000 --- a/src/main/angular/src/app/plot/plot.component.ts +++ /dev/null @@ -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[][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; - - @ViewChild('container') - chartContainer!: ElementRef; - - 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); - } - -} diff --git a/src/main/angular/src/app/plot/plot.service.ts b/src/main/angular/src/app/plot/plot.service.ts index 943eefe..675e34d 100644 --- a/src/main/angular/src/app/plot/plot.service.ts +++ b/src/main/angular/src/app/plot/plot.service.ts @@ -1,35 +1,36 @@ import {Injectable} from '@angular/core'; -import {ApiService, CrudService, Next} from '../COMMON'; +import {ApiService, EntityListService, Next} from '../COMMON'; import {Plot} from './Plot'; -import {Axis} from './Axis'; -import {Graph} from './Graph'; +import {Axis} from './axis/Axis'; +import {Graph} from './axis/graph/Graph'; import {Group} from './Group'; import {Interval} from '../series/Interval'; +import {GraphType} from './axis/graph/GraphType'; @Injectable({ providedIn: 'root' }) -export class PlotService extends CrudService { +export class PlotService extends EntityListService { constructor( api: ApiService, ) { - super(api, ["Plot"], Plot.fromJson); + super(api, ["Plot"], Plot.fromJson, Plot.equals, Plot.comparePosition); } - plotCreate(next: Next): void { + plotCreate(next?: Next): void { this.getSingle(["create"], next); } - plotDuplicate(plot: Plot, next: Next): void { + plotDuplicate(plot: Plot, next?: Next): void { this.getSingle([plot.id, "duplicate"], next); } - plotDelete(plot: Plot, next: Next): void { + plotDelete(plot: Plot, next?: Next): void { this.getSingle([plot.id, "delete"], next); } - plotAddAxis(plot: Plot, next: Next): void { + plotAddAxis(plot: Plot, next?: Next): void { this.getSingle([plot.id, 'addAxis'], next); } @@ -57,7 +58,7 @@ export class PlotService extends CrudService { this.postSingle([plot.id, 'position'], value, next); } - axisAddGraph(axis: Axis, next: Next): void { + axisAddGraph(axis: Axis, next?: Next): void { this.getSingle(['Axis', axis.id, 'addGraph'], next); } @@ -105,6 +106,10 @@ export class PlotService extends CrudService { this.postSingle(['Graph', graph.id, 'visible'], value, next); } + graphType(graph: Graph, value: GraphType, next?: Next): void { + this.postSingle(['Graph', graph.id, 'type'], value.jsonName, next); + } + graphName(graph: Graph, value: string, next?: Next): void { this.postSingle(['Graph', graph.id, 'name'], value, next); } diff --git a/src/main/angular/src/app/plot/plot/plot.component.html b/src/main/angular/src/app/plot/plot/plot.component.html new file mode 100644 index 0000000..4585a0f --- /dev/null +++ b/src/main/angular/src/app/plot/plot/plot.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/main/angular/src/app/plot/plot/plot.component.less b/src/main/angular/src/app/plot/plot/plot.component.less new file mode 100644 index 0000000..cbe226c --- /dev/null +++ b/src/main/angular/src/app/plot/plot/plot.component.less @@ -0,0 +1,4 @@ +.container { + width: 100%; + height: 100%; +} diff --git a/src/main/angular/src/app/plot/plot/plot.component.ts b/src/main/angular/src/app/plot/plot/plot.component.ts new file mode 100644 index 0000000..10651ae --- /dev/null +++ b/src/main/angular/src/app/plot/plot/plot.component.ts @@ -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[][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; + + @ViewChild('container') + protected chartContainer!: ElementRef; + + 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(); + } + } + +} diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts index 9df8c27..591cf4f 100644 --- a/src/main/angular/src/app/series/Series.ts +++ b/src/main/angular/src/app/series/Series.ts @@ -1,8 +1,8 @@ -import {mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON"; +import {ID, mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON"; import {SeriesType} from './SeriesType'; import {formatNumber} from '@angular/common'; -export class Series { +export class Series extends ID { constructor( readonly id: number, @@ -14,7 +14,7 @@ export class Series { readonly decimals: number, readonly value: number | null, ) { - // + super(); } static fromJson(json: any): Series { @@ -46,4 +46,7 @@ export class Series { return a.name.localeCompare(b.name); } + equals2() { + + } } diff --git a/src/main/angular/src/app/series/series.service.ts b/src/main/angular/src/app/series/series.service.ts index 145c4eb..8adac91 100644 --- a/src/main/angular/src/app/series/series.service.ts +++ b/src/main/angular/src/app/series/series.service.ts @@ -1,17 +1,17 @@ import {Injectable} from '@angular/core'; -import {ApiService, CrudService, Next, validateNumber} from "../COMMON"; +import {ApiService, EntityListService, Next, validateNumber} from "../COMMON"; import {Series} from './Series'; import {Interval} from './Interval'; @Injectable({ providedIn: 'root', }) -export class SeriesService extends CrudService { +export class SeriesService extends EntityListService { constructor( 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): void { diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 1fa18d7..9a6147b 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -3,6 +3,10 @@ html, body { margin: 0; } +body { + font-family: sans-serif; + margin-left: 2em; +} table { width: 100%; @@ -27,3 +31,22 @@ input, select { border: 1px solid black; 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; +} diff --git a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java index b180515..145f788 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/Graph.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/Graph.java @@ -50,6 +50,17 @@ public class Graph { @Column(nullable = false) 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 @NonNull @Column(nullable = false) diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java index fe98ebe..61824e2 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphController.java @@ -41,6 +41,11 @@ public class GraphController { 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") public PlotDto name(@PathVariable final long id, @RequestBody(required = false) @Nullable final String value) { return graphService.set(id, graph -> graph.setName(or(value, ""))); diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java index 5666fcc..6d30ad5 100644 --- a/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphDto.java @@ -19,6 +19,10 @@ public class GraphDto { public final boolean visible; + public final GraphType type; + + public final String fill; + @NonNull public final String color; @@ -42,6 +46,8 @@ public class GraphDto { this.series = new SeriesDto(graph.getSeries(), false); this.name = graph.getName(); this.visible = graph.isVisible(); + this.type = graph.getType(); + this.fill = graph.getFill(); this.color = graph.getColor(); this.factor = graph.getFactor(); this.group = graph.getGroup(); diff --git a/src/main/java/de/ph87/data/plot/axis/graph/GraphType.java b/src/main/java/de/ph87/data/plot/axis/graph/GraphType.java new file mode 100644 index 0000000..902b885 --- /dev/null +++ b/src/main/java/de/ph87/data/plot/axis/graph/GraphType.java @@ -0,0 +1,5 @@ +package de.ph87.data.plot.axis.graph; + +public enum GraphType { + LINE, BAR +}