import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { DataType, AxesTypes, PlotResponseStats, PlotConfig, PlotService, PlotResponse, PlotRequest, RenderOptions } from '../plot.service'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import * as moment from 'moment'; import { WidgetDimensions } from '../dashboard.service'; @Component({ selector: 'pdb-plot-view', templateUrl: './plot-view.component.html', styleUrls: ['./plot-view.component.scss'] }) export class PlotViewComponent implements OnInit { 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 imageUrl! : string; stats: PlotResponseStats | null = null; axes!: AxesTypes; @Output() loadingEvent : EventEmitter = new EventEmitter(); @Output() dateRangeUpdateEvent : EventEmitter = new EventEmitter(); 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; constructor(private service : PlotService, private snackBar: MatSnackBar) { } ngOnInit() { } 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 = (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.format(this.DATE_PATTERN); const formattedEndDate = endDate.format(this.DATE_PATTERN); const newDateRange = formattedStartDate+" - "+formattedEndDate; //(document.getElementById("search-date-range")).value = newDateRange; this.dateRangeUpdateEvent.emit(newDateRange); //this.plot(); } 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(); const y = new Array(); 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.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( (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(dateRange:string, anchorInPercentOfDateRange:number, zoomFactor:number) { const dateRangeParsed = this.parseDateRange(dateRange); const dateRangeInSeconds = dateRangeParsed.duration.asSeconds(); const anchorTimestampInSeconds = dateRangeParsed.startDate.clone().add(Math.floor(dateRangeInSeconds*anchorInPercentOfDateRange), "seconds"); const newDateRangeInSeconds = dateRangeInSeconds * zoomFactor; const newStartDate = anchorTimestampInSeconds.clone().subtract(newDateRangeInSeconds*anchorInPercentOfDateRange, "seconds"); const newEndDate = newStartDate.clone().add({seconds: newDateRangeInSeconds});; this.setDateRange(newStartDate, newEndDate); } /** * 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(dateRange: string, factorStartDate: number, factorEndDate: number) { const dateRangeParsed = this.parseDateRange(dateRange); const dateRangeInSeconds = dateRangeParsed.duration.asSeconds(); const newStartDate = dateRangeParsed.startDate.add({seconds: dateRangeInSeconds*factorStartDate}); const newEndDate = dateRangeParsed.endDate.add({seconds: dateRangeInSeconds*factorEndDate}); this.setDateRange(newStartDate, newEndDate); } parseDateRange(dateRangeAsString : string) : DateRange { const startDate = moment(dateRangeAsString.slice(0, 19)); const endDate = moment(dateRangeAsString.slice(22, 41)); return { startDate: startDate, endDate: endDate, duration: moment.duration(endDate.diff(startDate)) }; } } 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 { startDate: any; endDate: any; duration: any; }