405 lines
13 KiB
TypeScript
405 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)
|
|
{
|
|
debugger;
|
|
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)
|
|
{
|
|
debugger;
|
|
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){}
|
|
} |