Files
perfdb/pdb-js/src/app/plot-view/plot-view.component.ts
2024-09-28 10:26:38 +02:00

403 lines
13 KiB
TypeScript

import { Component, Output, EventEmitter } from '@angular/core';
import { DataType, AxesTypes, PlotResponseStats, PlotConfig, PlotService, PlotResponse, PlotRequest, RenderOptions, DataSeriesStats, DashTypeAndColor } from '../plot.service';
import { MatSnackBar } from '@angular/material/snack-bar';
//import * as moment from 'moment';
import { WidgetDimensions } from '../dashboard.service';
import { Overlay } from "@angular/cdk/overlay";
import { DateTime, Duration } from "luxon";
import { DateValue } from '../components/datepicker/date-picker.component';
import { Observable } from 'rxjs';
@Component({
selector: 'pdb-plot-view',
templateUrl: './plot-view.component.html',
styleUrls: ['./plot-view.component.scss']
})
export class PlotViewComponent {
readonly DATE_PATTERN = "yyyy-MM-dd HH:mm:ss"; // for moment-JS
readonly gnuplotLMargin = 110; // The left margin configured for gnuplot
readonly gnuplotRMargin = 110; // The right margin configured for gnuplot
readonly gnuplotTMargin = 57; // The top margin configured for gnuplot
readonly gnuplotBMargin = 76; // The bottom margin configured for gnuplot
isOpen = false;
imageUrl! : string;
stats: PlotResponseStats | null = null;
axes!: AxesTypes;
@Output()
loadingEvent : EventEmitter<LoadingEvent> = new EventEmitter<LoadingEvent>();
@Output()
dateRangeUpdateEvent : EventEmitter<DateValue> = new EventEmitter<DateValue>();
in_drag_mode = false;
drag_start_x = 0;
drag_start_y = 0;
drag_end_x = 0;
imageCursor = "default";
zoomInSliderStyleDisplay = "none";
zoomInSliderStyleTopMargin = this.gnuplotTMargin+"px";
zoomInSliderStyleBottomMargin = this.gnuplotBMargin+"px";
zoomInSliderStyleLeft = "0";
zoomInSliderStyleWidth = "0";
showStats = false;
config? : PlotConfig;
legendInitialPosition = {x:115,y:60};
constructor(private service : PlotService, private snackBar: MatSnackBar, private overlay: Overlay) { }
showError(message:string) {
this.snackBar.open(message, "", {
duration: 5000,
verticalPosition: 'top'
});
}
hideZoomInSlider() {
this.zoomInSliderStyleDisplay = "none";
}
update_cursor(event: MouseEvent){
//$('#result-image').css('cursor', this.isInPlot(event) ? 'crosshair' : 'default');
this.imageCursor = this.isInPlot(event) ? 'crosshair' : 'default';
}
imageWidth() {
return Math.floor(document.getElementById('result-image')!.offsetWidth);
}
imageHeight() {
return Math.floor(document.getElementById('result-image')!.offsetHeight);
}
positionInImage(event: MouseEvent) : any {
const rect = (<HTMLImageElement>document.getElementById('result-image')).getBoundingClientRect();
const x= event.clientX - rect.left;
const y= event.clientY - rect.top;
//console.log("pos: " + x+"x" +y+ " rect: "+rect.x+"x"+rect.y + " client: "+ event.clientX+"x"+ event.clientY + " offset:" + event.offsetX+"x"+event.offsetY );
//console.log(JSON.stringify(rect));
return {x: x, y: y};
}
isInPlot(event: MouseEvent) : boolean{
const pos = this.positionInImage(event);
return pos.x > this.gnuplotLMargin
&& pos.x < this.imageWidth() - this.gnuplotRMargin
&& pos.y > this.gnuplotTMargin
&& pos.y < this.imageHeight()- this.gnuplotBMargin;
}
isInImage(event: MouseEvent) : boolean{
const pos = this.positionInImage(event);
return pos.x > 0
&& pos.x < this.imageWidth()
&& pos.y > 0
&& pos.y < this.imageHeight();
}
dragStart(event: MouseEvent) {
//console.log("dragStart inPlot: " + this.isInPlot(event));
event.preventDefault();
if (event.buttons == 1 && this.isInPlot(event) && this.axes.hasXAxis(DataType.Time)) {
const pos = this.positionInImage(event);
this.in_drag_mode = true;
this.drag_start_x = pos.x;
this.drag_end_x = pos.x;
this.drag_start_y = pos.y;
}
}
dragging(event: MouseEvent) {
//console.log("dragging " + this.isInPlot(event));
this.update_cursor(event);
if (this.in_drag_mode && event.buttons == 1){
const pos = this.positionInImage(event);
this.drag_end_x = Math.max(Math.min(pos.x, this.imageWidth()-this.gnuplotRMargin), this.gnuplotLMargin);
const left = this.drag_start_x < this.drag_end_x ? this.drag_start_x : this.drag_end_x;
const width = Math.abs(this.drag_start_x - this.drag_end_x);
if (width > 10) {
this.zoomInSliderStyleDisplay = "block";
this.zoomInSliderStyleLeft= left+"px";
this.zoomInSliderStyleWidth= width+"px";
this.zoomInSliderStyleBottomMargin = "calc(100% - "+(this.imageHeight()-this.gnuplotBMargin)+"px)";
} else {
this.hideZoomInSlider();
}
}
}
dragStop(event: MouseEvent) {
if (this.in_drag_mode){
this.in_drag_mode = false;
this.hideZoomInSlider();
// Zoom in if the selected area has some arbitrary minimal size
if (Math.abs(this.drag_start_x - this.drag_end_x) > 10) {
const startPxInImage = Math.min(this.drag_start_x, this.drag_end_x);
const endPxInImage = Math.max(this.drag_start_x, this.drag_end_x);
const imageWidth = this.imageWidth();
const widthPlotArea = imageWidth - this.gnuplotLMargin - this.gnuplotRMargin;
const startPxWithinPlotArea = startPxInImage - this.gnuplotLMargin;
const endPxWithinPlotArea = endPxInImage - this.gnuplotLMargin;
const startPercentOfDateRange = startPxWithinPlotArea / widthPlotArea;
const endPercentOfDateRange = endPxWithinPlotArea / widthPlotArea;
this.zoomRange(new SelectionRange(startPercentOfDateRange, endPercentOfDateRange));
}
}
}
dragAbort(event: MouseEvent) {
//console.log("drag_abort");
if (this.in_drag_mode && !this.isInImage(event)) {
this.in_drag_mode = false;
this.drag_start_x = 0;
this.drag_end_x = 0;
this.hideZoomInSlider();
}
}
setDateRange(startDate: any, endDate: any) {
const formattedStartDate = startDate.toFormat(this.DATE_PATTERN);
const formattedEndDate = endDate.toFormat(this.DATE_PATTERN);
const newDateRange = formattedStartDate+" - "+formattedEndDate;
const newDateValue = new DateValue('ABSOLUTE', newDateRange, newDateRange);
this.dateRangeUpdateEvent.emit(newDateValue);
}
zoomRange(range: SelectionRange) {
this.shiftDate(this.config?.dateRange!, range.startPercentOfDateRange, range.endPercentOfDateRange-1);
}
zoomWithDateAnchor(dateAnchor: DateAnchor){
this.shiftDateByAnchor(this.config?.dateRange!, dateAnchor.cursorPercentOfDateRange, dateAnchor.zoomFactor);
}
zoomByScroll(event: WheelEvent) {
if (this.isInImage(event) && event.deltaY != 0 && this.axes.hasXAxis(DataType.Time)) {
this.in_drag_mode = false;
this.hideZoomInSlider();
const pos = this.positionInImage(event);
const widthPlotArea = this.imageWidth() - this.gnuplotLMargin - this.gnuplotRMargin;
const cursorPxWithinPlotArea = pos.x - this.gnuplotLMargin;
const cursorPercentOfDateRange = cursorPxWithinPlotArea / widthPlotArea;
const zoomFactor = event.deltaY < 0 ? 0.5 : 2;
this.zoomWithDateAnchor(new DateAnchor(cursorPercentOfDateRange, zoomFactor));
}
}
showDetails() {
this.showStats = true;
}
hideDetails() {
this.showStats = false;
}
getAxes(): AxesTypes{
const plotTypes = this.service.getPlotTypes();
this.config?.aggregates
const x = new Array<DataType>();
const y = new Array<DataType>();
for(var i = 0; i < this.config!.aggregates.length; i++){
const aggregateType = this.config?.aggregates[i];
const plotType = plotTypes.find((p) => p.id == aggregateType);
if (plotType) {
if (!x.includes(plotType.xAxis)) {
x.push(plotType.xAxis);
}
if (!y.includes(plotType.yAxis)) {
y.push(plotType.yAxis);
}
}
}
return new AxesTypes(x,y);
}
plot(config : PlotConfig, dimension: WidgetDimensions | (()=>WidgetDimensions)){
this.config = config;
this.axes = this.getAxes();
const request = this.createPlotRequest(dimension);
this.imageUrl = '';
this.stats = null;
document.dispatchEvent(new Event("invadersStart", {}));
this.loadingEvent.emit(new LoadingEvent(true));
const x = this.service.sendPlotRequest(request).subscribe({
next: (plotResponse: PlotResponse) => {
this.imageUrl = "http://"+window.location.hostname+':'+window.location.port+'/'+plotResponse.rendered['main'];
this.stats = plotResponse.stats;
document.dispatchEvent(new Event("invadersPause", {}));
this.loadingEvent.emit(new LoadingEvent(false));
},
error: (error:any) => {
this.imageUrl = '';
this.stats = null;
this.showError(error.error.message);
document.dispatchEvent(new Event("invadersPause", {}));
this.loadingEvent.emit(new LoadingEvent(false));
}
});
}
createPlotRequest( dimension: WidgetDimensions | (()=>WidgetDimensions)): PlotRequest {
const actualDimension = typeof dimension === "function" ? dimension() : dimension;
const request = new PlotRequest(
(<any>window).submitterId,
this.config!,
{
'main': new RenderOptions(actualDimension.height, actualDimension.width, false, true)
});
return request;
}
/**
* Zoom in/out by zoomFaktor, so that the anchorInPercentOfDateRange keeps the same position.
*
* shiftDateByAnchor(dateRangeAsString, 0.20, 0.5) zooms in by 50%, so that the date that was at 20% before the zoom is still at 20% after the zoom
* shiftDateByAnchor(dateRangeAsString, 0.33, 2) zooms out by 50%, so that the date that was at 33% before the zoom is still at 33% after the zoom
*/
shiftDateByAnchor(dateValue:DateValue, anchorInPercentOfDateRange:number, zoomFactor:number)
{
const dateRangeParsed = this.parseDateRange(dateValue);
dateRangeParsed.subscribe({
next: (dataRange: DateRange) => {
const dateRangeInSeconds = Math.floor(dataRange.duration.toMillis()/1000);
const anchorTimestampInSeconds = dataRange.startDate.plus(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange)*1000);
const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor;
const newStartDate = anchorTimestampInSeconds.minus(newDateRangeInSeconds*anchorInPercentOfDateRange*1000);
const newEndDate = newStartDate.plus({seconds: newDateRangeInSeconds});;
this.setDateRange(newStartDate, newEndDate);
},
error: (err: any) => {
window.console.error("failed to parse DateValue into DateRange: ", err);
}
})
}
/**
* Zoom in/out or shift date by adding factorStartDate*dateRangeInSeconds seconds to the start date
* and factorEndDate*dateRangeInSeconds seconds to the end date.
*
* shiftDate(dateRangeAsString, 0.25, -0.25) will zoom in, making the range half its size
* shiftDate(dateRangeAsString, -0.5, 0.5) will zoom out, making the range double its size
* shiftDate(dateRangeAsString, -0.5, -0.5) will move the range by half its size to older values
* shiftDate(dateRangeAsString, 1, 1) will move the range by its size to newer values
*/
shiftDate(dateValue: DateValue, factorStartDate: number, factorEndDate: number)
{
this.parseDateRange(dateValue).subscribe(
dateRangeParsed => {
const dateRangeInSeconds = Math.floor(dateRangeParsed.duration.toMillis()/1000);
const newStartDate = dateRangeParsed.startDate.plus({seconds: dateRangeInSeconds*factorStartDate});
const newEndDate = dateRangeParsed.endDate.plus({seconds: dateRangeInSeconds*factorEndDate});
this.setDateRange(newStartDate, newEndDate);
}
);
}
parseDateRange(dateValue : DateValue) : Observable<DateRange> {
return this.service.toDateRange(dateValue);
/*
.pipe(map((dateRangeAsString:string) => {
const startDate = DateTime.fromFormat(dateRangeAsString.slice(0, 19), this.DATE_PATTERN );
const endDate = DateTime.fromFormat(dateRangeAsString.slice(22, 41), this.DATE_PATTERN );
return {
startDate: startDate,
endDate: endDate,
duration: endDate.diff(startDate),
};
}));
*/
}
dataSeries(): Array<DataSeriesStats> {
return this.stats ? this.stats.dataSeriesStats : [];
}
pointTypeClass(typeAndColor: DashTypeAndColor): string {
return "plot-details-plotType"
+" plot-details-plotType_"+typeAndColor.pointType
+" plot-details-plotType_"+typeAndColor.color.toLocaleLowerCase();
}
}
export class SelectionRange {
startPercentOfDateRange : number;
endPercentOfDateRange : number;
constructor(startPercentOfDateRange: number, endPercentOfDateRange: number){
this.startPercentOfDateRange = startPercentOfDateRange;
this.endPercentOfDateRange = endPercentOfDateRange;
}
}
export class DateAnchor {
cursorPercentOfDateRange : number;
zoomFactor : number;
constructor(cursorPercentOfDateRange : number, zoomFactor : number) {
this.cursorPercentOfDateRange = cursorPercentOfDateRange;
this.zoomFactor = zoomFactor;
}
}
export class LoadingEvent {
constructor(public loading: boolean){}
}
export class DateRange {
constructor(
public startDate: DateTime,
public endDate: DateTime,
public duration: Duration){}
}