You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by eb...@apache.org on 2020/05/18 11:49:13 UTC

[incubator-streampipes] 04/04: Integrate 'timeseries labeling tool' into new data explorer component

This is an automated email from the ASF dual-hosted git repository.

ebi pushed a commit to branch STREAMPIPES-79
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git

commit 2168eb8e61fdb874f1a3c60c74ba355e24a13996
Author: Daniel Ebi <eb...@fzi.de>
AuthorDate: Mon May 18 12:08:03 2020 +0200

    Integrate 'timeseries labeling tool' into new data explorer component
---
 .../rest/impl/datalake/DataLakeResourceV3.java     |   2 +-
 .../datalake/datalake-rest.service.ts              |   6 +
 .../line-chart/line-chart-widget.component.html    |   6 +-
 .../line-chart/line-chart-widget.component.ts      | 373 ++++++++++++++++++++-
 .../data-explorer-v2/data-explorer-v2.module.ts    |  12 +-
 5 files changed, 382 insertions(+), 17 deletions(-)

diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResourceV3.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResourceV3.java
index a14a74d..846c577 100644
--- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResourceV3.java
+++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeResourceV3.java
@@ -231,7 +231,7 @@ public class DataLakeResourceV3 extends AbstractRestInterface {
   public Response getImageCoco(@PathParam("route") String fileRoute) throws IOException {
     return ok(dataLakeManagement.getImageCoco(fileRoute));
   }
-  
+
   @POST
   @Produces(MediaType.TEXT_PLAIN)
   @Path("/data/{index}/{startdate}/{enddate}/labeling")
diff --git a/ui/src/app/core-services/datalake/datalake-rest.service.ts b/ui/src/app/core-services/datalake/datalake-rest.service.ts
index fce1fbd..6cc927d 100644
--- a/ui/src/app/core-services/datalake/datalake-rest.service.ts
+++ b/ui/src/app/core-services/datalake/datalake-rest.service.ts
@@ -135,6 +135,12 @@ export class DatalakeRestService {
         };
     }
 
+    get_timeseries_labels() {
+        // mocked labels
+        const labels = {state: ['online', 'offline', 'active', 'inactive'], trend: ['increasing', 'decreasing'], daytime: ['day', 'night']};
+        return labels;
+    }
+
     getImageUrl(imageRoute) {
       return this.dataLakeUrlV3 + '/data/image/' + imageRoute + '/file';
     }
diff --git a/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.html b/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.html
index d616ff6..b6fdd3c 100644
--- a/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.html
+++ b/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.html
@@ -37,11 +37,13 @@
 
             <!-- Chart -->
             <plotly-plot fxFlex
-                         *ngIf="showData"
+                         *ngIf="data !== undefined"
                          flex
                          [data]="data"
                          [layout]="graph.layout"
-                         (relayout)="zoomIn($event)">
+                         [config]="graph.config"
+                         (relayout)="handleDefaultModeBarButtonClicks($event)"
+                         (selecting)="selectDataPoints($event)">
             </plotly-plot>
 
         </div>
diff --git a/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.ts b/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.ts
index 2b22ca1..2a4bd0f 100644
--- a/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.ts
+++ b/ui/src/app/data-explorer-v2/components/widgets/line-chart/line-chart-widget.component.ts
@@ -16,10 +16,15 @@
  *
  */
 
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, Renderer2 } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { PlotlyService } from 'angular-plotly.js';
 import { EventProperty } from '../../../../connect/schema-editor/model/EventProperty';
 import { DataResult } from '../../../../core-model/datalake/DataResult';
 import { DatalakeRestService } from '../../../../core-services/datalake/datalake-rest.service';
+import { ChangeChartmodeDialog } from '../../../../core-ui/linechart/labeling-tool/dialogs/change-chartmode/change-chartmode.dialog';
+import { LabelingDialog } from '../../../../core-ui/linechart/labeling-tool/dialogs/labeling/labeling.dialog';
+import { ColorService } from '../../../../core-ui/linechart/labeling-tool/services/color.service';
 import { BaseDataExplorerWidget } from '../base/base-data-explorer-widget';
 
 @Component({
@@ -35,22 +40,22 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
   yKeys: string[] = [];
   xKey: string;
 
-  constructor(protected dataLakeRestService: DatalakeRestService) {
+  selectedStartX = undefined;
+  selectedEndX = undefined;
+  n_selected_points = undefined;
+
+  constructor(public dialog: MatDialog, public plotlyService: PlotlyService, public colorService: ColorService,
+              public renderer: Renderer2, protected dataLakeRestService: DatalakeRestService) {
     super(dataLakeRestService);
   }
 
+  // indicator variable if labeling mode is activated
+  private labelingModeOn = false;
 
-  ngOnInit(): void {
-
-    this.availableColumns = this.getNumericProperty(this.dataExplorerWidget.dataLakeMeasure.eventSchema);
-    // Reduce selected columns when more then 6
-    this.selectedColumns = this.availableColumns.length > 6 ? this.availableColumns.slice(0, 5) : this.availableColumns;
-
-    this.xKey = this.getTimestampProperty(this.dataExplorerWidget.dataLakeMeasure.eventSchema).runtimeName;
-    this.yKeys = this.getRuntimeNames(this.selectedColumns);
-    this.updateData();
-  }
+  // indicator variable if labels has been changed
+  private changedLabels = false;
 
+  private dialogReference = undefined;
 
   updatemenus = [
     {
@@ -98,9 +103,38 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
         fixedrange: true
       },
       updatemenus: this.updatemenus,
+
+      // setting hovermode to 'closest'
+      hovermode: 'closest',
+      // adding shapes for displaying labeled time intervals
+      shapes: [],
+      // box selection with fixed height
+      selectdirection: 'h',
+
+      // default dragmode is zoom
+      dragmode: 'zoom'
+    },
+    config: {
+      // removing lasso-selection, box-selecting, toggling-spikelines and exporting-to-image buttons
+      modeBarButtonsToRemove: ['lasso2d', 'select2d', 'toggleSpikelines', 'toImage'],
+      // adding custom button: labeling
+      modeBarButtonsToAdd: [this.createLabelingModeBarButton()],
+      // removing plotly-icon from graph
+      displaylogo: false
     }
   };
 
+  
+  ngOnInit(): void {
+
+    this.availableColumns = this.getNumericProperty(this.dataExplorerWidget.dataLakeMeasure.eventSchema);
+    // Reduce selected columns when more then 6
+    this.selectedColumns = this.availableColumns.length > 6 ? this.availableColumns.slice(0, 5) : this.availableColumns;
+
+    this.xKey = this.getTimestampProperty(this.dataExplorerWidget.dataLakeMeasure.eventSchema).runtimeName;
+    this.yKeys = this.getRuntimeNames(this.selectedColumns);
+    this.updateData();
+  }
 
 
   updateData() {
@@ -109,11 +143,20 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
     this.dataLakeRestService.getDataAutoAggergation(
       this.dataExplorerWidget.dataLakeMeasure.measureName, this.viewDateRange.startDate.getTime(), this.viewDateRange.endDate.getTime())
       .subscribe((res: DataResult) => {
+
         if (res.total === 0) {
           this.setShownComponents(true, false, false);
         } else {
+          res.measureName = this.dataExplorerWidget.dataLakeMeasure.measureName;
           const tmp = this.transformData(res, this.xKey);
           this.data = this.displayData(tmp, this.yKeys);
+          this.data['measureName'] = tmp.measureName;
+          this.data['labels'] = tmp.labels;
+
+          if (this.data['labels'] !== undefined && this.data['labels'].length > 0) {
+            this.addInitialColouredShapesToGraph();
+          }
+
           this.setShownComponents(false, true, false);
         }
 
@@ -122,7 +165,6 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
   }
 
 
-
   displayData(transformedData: DataResult, yKeys: string[]) {
     if (this.yKeys.length > 0) {
       const tmp = [];
@@ -130,6 +172,16 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
         transformedData.rows.forEach(serie => {
           if (serie.name === key) {
             tmp.push(serie);
+
+            // adding customdata property in order to store labels in graph
+            if (transformedData.labels !== undefined && transformedData.labels.length !== 0) {
+              serie['customdata'] = transformedData.labels;
+            } else {
+              serie['customdata'] = Array(serie['x'].length).fill('');
+            }
+            // adding custom hovertemplate in order to display labels in graph
+            serie['hovertemplate'] = 'y: %{y}<br>' + 'x: %{x}<br>' + 'label: %{customdata}';
+
           }
         });
       });
@@ -145,16 +197,21 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
     const tmp: any[] = [];
 
     const dataKeys = [];
+    const label_column = [];
 
     data.rows.forEach(row => {
       data.headers.forEach((headerName, index) => {
         if (!dataKeys.includes(index) && typeof row[index] === 'number') {
           dataKeys.push(index);
         }
+        else if (!label_column.includes(index) && typeof  row[index] == 'string' && data.headers[index] == "sp_internal_label") {
+          label_column.push(index);
+        }
       });
     });
 
     const indexXkey = data.headers.findIndex(headerName => headerName === this.xKey);
+    const labels = [];
 
     dataKeys.forEach(key => {
       const headerName = data.headers[key];
@@ -170,10 +227,13 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
           } else {
             tmp[index].y.push(null);
           }
+        } else if (label_column.length > 0 && label_column.includes(index)) {
+          labels.push(row[index]);
         }
       });
     });
     data.rows = tmp;
+    data.labels = labels;
 
     return data;
   }
@@ -183,4 +243,291 @@ export class LineChartWidgetComponent extends BaseDataExplorerWidget implements
     this.yKeys = this.getRuntimeNames(selectedColumns);
     this.updateData();
   }
+
+  handleDefaultModeBarButtonClicks($event) {
+    if (!('xaxis.autorange' in $event) && !('hovermode' in $event)) {
+      if ($event.dragmode !== 'select') {
+        this.deactivateLabelingMode();
+        this.labelingModeOn = false;
+      }
+    } else if (($event['xaxis.autorange'] === true || $event['hovermode'] === true) && this.labelingModeOn) {
+      this.activateLabelingMode();
+    }
+  }
+
+  selectDataPoints($event) {
+    // getting selected time interval
+    const xStart = $event['range']['x'][0];
+    const xEnd = $event['range']['x'][1];
+
+    // updating related global time interval properties
+    this.setStartX(xStart);
+    this.setEndX(xEnd);
+
+    // getting number of selected data points
+    let selected_points = 0;
+    for (const series of this.data) {
+      if (series['selectedpoints'] !== undefined) {
+        selected_points = selected_points + series['selectedpoints'].length;
+      }
+    }
+
+    // updating related global variable
+    this.setNSelectedPoints(selected_points);
+
+    // opening Labeling-Dialog
+    this.openLabelingDialog();
+    this.dialogReference.componentInstance.data = {labels: this.dataLakeRestService.get_timeseries_labels(), selected_label: '',
+      startX: this.selectedStartX, endX: this.selectedEndX, n_selected_points: this.n_selected_points};
+  }
+
+  private openLabelingDialog() {
+    if (this.dialog.openDialogs.length === 0) {
+
+      // displaying Info-Dialog 'Change Chart-Mode' if current graph mode is 'lines'
+      if (this.data[0]['mode'] === 'lines') {
+
+        // deactivating labeling mode
+        this.labelingModeOn = false;
+        this.deactivateLabelingMode();
+
+        const dialogRef = this.dialog.open(ChangeChartmodeDialog,
+            {
+              width: '400px',
+              position: {top: '150px'}
+            });
+
+        this.dialogReference = dialogRef;
+
+        // displaying Labeling-Dialog, obtaining selected label and drawing coloured shape
+      } else {
+        const dialogRef = this.dialog.open(LabelingDialog,
+            {
+              width: '400px',
+              height: 'auto',
+              position: {top: '75px'},
+              data: {labels: this.dataLakeRestService.get_timeseries_labels(), selected_label: '', startX: this.selectedStartX, endX:
+                this.selectedEndX, n_selected_points: this.n_selected_points}
+            });
+
+        this.dialogReference = dialogRef;
+
+        // after closing Labeling-Dialog
+        dialogRef.afterClosed().subscribe(result => {
+
+          // adding selected label to displayed data points
+          if (result !== undefined) {
+            for (const series of this.data) {
+              for (const point of series['selectedpoints']) {
+                series['customdata'][point] = result;
+              }
+            }
+            this.data['labels'] = this.data[0]['customdata'];
+            this.setChangedLabels(true);
+
+            // adding coloured shape (based on selected label) to graph (equals selected time interval)
+            this.addShapeToGraph(this.selectedStartX, this.selectedEndX, this.colorService.getColor(result));
+
+            // remain in selection dragmode if labeling mode is still activated
+            if (this.labelingModeOn) {
+              this.graph.layout.dragmode = 'select';
+            } else {
+              this.graph.layout.dragmode = 'zoom';
+            }
+          }
+        });
+      }
+    }
+  }
+
+  private createLabelingModeBarButton() {
+    const labelingModeBarButton = {
+      name: 'Labeling',
+      icon: this.plotlyService.getPlotly().Icons.pencil,
+      direction: 'up',
+      click: (gd) => {
+
+        // only allowing to activate labeling mode if current graph mode does not equal 'lines'
+        if (this.data[0]['mode'] !== 'lines') {
+          this.labelingModeOn = !this.labelingModeOn;
+
+          // activating labeling mode
+          if (this.labelingModeOn) {
+            this.activateLabelingMode();
+
+            // deactivating labeling mode
+          } else {
+            this.deactivateLabelingMode();
+          }
+
+          // otherwise displaying 'Change Chart Mode Dialog' or deactivating labeling mode
+        } else {
+          if (this.labelingModeOn) {
+            this.labelingModeOn = !this.labelingModeOn;
+            this.deactivateLabelingMode();
+          } else {
+            this.openLabelingDialog();
+          }
+        }
+      }
+    };
+    return labelingModeBarButton;
+  }
+
+  private activateLabelingMode() {
+    const modeBarButtons = document.getElementsByClassName('modebar-btn');
+
+    for (let i = 0; i < modeBarButtons.length; i++) {
+      if (modeBarButtons[i].getAttribute('data-title') === 'Labeling') {
+
+        // fetching path of labeling button icon
+        const path = modeBarButtons[i].getElementsByClassName('icon').item(0)
+            .getElementsByTagName('path').item(0);
+
+        // adding 'clicked' to class list
+        modeBarButtons[i].classList.add('clicked');
+
+        // changing color of fetched path
+        this.renderer.setStyle(path, 'fill', '#39B54A');
+      }
+    }
+
+    // changing dragmode to 'select'
+    this.graph.layout.dragmode = 'select';
+  }
+
+  private deactivateLabelingMode() {
+    const modeBarButtons = document.getElementsByClassName('modebar-btn');
+
+    for (let i = 0; i < modeBarButtons.length; i++) {
+      if (modeBarButtons[i].getAttribute('data-title') === 'Labeling') {
+
+        // fetching path of labeling button icon
+        const path = modeBarButtons[i].getElementsByClassName('icon').item(0)
+            .getElementsByTagName('path').item(0);
+
+        // removing 'clicked' from class list
+        modeBarButtons[i].classList.remove('clicked');
+
+        // changing path color to default plotly modebar button color
+        this.renderer.setStyle(path, 'fill', 'rgba(68, 68, 68, 0.3)');
+      }
+    }
+
+    // changing dragmode to 'zoom'
+    this.graph.layout.dragmode = 'zoom';
+
+    // saving labels persistently
+    if (this.getChangedLabels()) {
+      this.saveLabelsInDatabase();
+    }
+  }
+
+  private saveLabelsInDatabase() {
+    let currentLabel = undefined;
+    let indices = [];
+    for (const label in this.data['labels']) {
+      if (currentLabel !== this.data['labels'][label] && indices.length > 0) {
+        const startdate = new Date(this.data[0]['x'][indices[0]]).getTime() - 1;
+        const enddate = new Date(this.data[0]['x'][indices[indices.length - 1]]).getTime() + 1;
+        this.dataLakeRestService.saveLabelsInDatabase(this.data['measureName'], startdate, enddate, currentLabel).subscribe(
+            res => {
+              // console.log('Successfully wrote label ' + currentLabel + ' into database.');
+            }
+        );
+        currentLabel = undefined;
+        indices = [];
+        indices.push(label);
+      } else {
+        indices.push(label);
+      }
+
+      currentLabel = this.data['labels'][label];
+    }
+    const last_startdate = new Date(this.data[0]['x'][indices[0]]).getTime() - 1;
+    const last_enddate = new Date(this.data[0]['x'][indices[indices.length - 1]]).getTime() + 1;
+    this.dataLakeRestService.saveLabelsInDatabase(this.data['measureName'], last_startdate, last_enddate, currentLabel).subscribe(
+        res => {
+          // console.log('Successfully wrote label ' + currentLabel + ' in last iteration into database.');
+        });
+    this.setChangedLabels(false);
+
+  }
+
+  private addInitialColouredShapesToGraph() {
+    let selectedLabel = undefined;
+    let indices = [];
+    for (const label in this.data['labels']) {
+      if (selectedLabel !== this.data['labels'][label] && indices.length > 0) {
+        const startdate = new Date(this.data[0]['x'][indices[0]]).getTime();
+        const enddate = new Date(this.data[0]['x'][indices[indices.length - 1]]).getTime();
+        const color = this.colorService.getColor(selectedLabel);
+
+        this.addShapeToGraph(startdate, enddate, color);
+
+        selectedLabel = undefined;
+        indices = [];
+        indices.push(label);
+      } else {
+        indices.push(label);
+      }
+      selectedLabel = this.data['labels'][label];
+    }
+    const last_start = new Date(this.data[0]['x'][indices[0]]).getTime();
+    const last_end = new Date(this.data[0]['x'][indices[indices.length - 1]]).getTime();
+    const last_color = this.colorService.getColor(selectedLabel);
+
+    this.addShapeToGraph(last_start, last_end, last_color);
+  }
+
+  private addShapeToGraph(start, end, color) {
+    const shape = {
+      // shape: rectangle
+      type: 'rect',
+
+      // x-reference is assigned to the x-values
+      xref: 'x',
+
+      // y-reference is assigned to the plot paper [0,1]
+      yref: 'paper',
+      y0: 0,
+      y1: 1,
+
+      // start x: left side of selected time interval
+      x0: start,
+      // end x: right side of selected time interval
+      x1: end,
+
+      // adding color
+      fillcolor: color,
+
+      // opacity of 20%
+      opacity: 0.2,
+
+      line: {
+        width: 0
+      }
+    };
+    this.graph.layout.shapes.push(shape);
+  }
+
+  public setChangedLabels(state: boolean) {
+    this.changedLabels = state;
+  }
+
+  public getChangedLabels() {
+    return this.changedLabels;
+  }
+
+  setStartX(startX: string) {
+    this.selectedStartX = startX;
+  }
+
+  setEndX(endX: string) {
+    this.selectedEndX = endX;
+  }
+
+  setNSelectedPoints(n_selected_points: number) {
+    this.n_selected_points = n_selected_points;
+  }
 }
diff --git a/ui/src/app/data-explorer-v2/data-explorer-v2.module.ts b/ui/src/app/data-explorer-v2/data-explorer-v2.module.ts
index d4dbed5..55ef8b6 100644
--- a/ui/src/app/data-explorer-v2/data-explorer-v2.module.ts
+++ b/ui/src/app/data-explorer-v2/data-explorer-v2.module.ts
@@ -21,8 +21,12 @@ import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
 import { FlexLayoutModule } from '@angular/flex-layout';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatChipsModule } from '@angular/material/chips';
+import { MatNativeDateModule } from '@angular/material/core';
+import { MatDatepickerModule } from '@angular/material/datepicker';
 import { MatGridListModule } from '@angular/material/grid-list';
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatSliderModule } from '@angular/material/slider';
 import { MatSnackBarModule } from '@angular/material/snack-bar';
 import { MatTabsModule } from '@angular/material/tabs';
 import { OWL_DATE_TIME_FORMATS, OwlDateTimeModule, OwlNativeDateTimeModule } from '@danielmoncada/angular-datetime-picker';
@@ -36,6 +40,7 @@ import { ConnectModule } from '../connect/connect.module';
 import { SemanticTypeUtilsService } from '../core-services/semantic-type/semantic-type-utils.service';
 import { SharedDatalakeRestService } from '../core-services/shared/shared-dashboard.service';
 import { CoreUiModule } from '../core-ui/core-ui.module';
+import { LabelingToolModule } from '../core-ui/linechart/labeling-tool/labeling-tool.module';
 import { CustomMaterialModule } from '../CustomMaterial/custom-material.module';
 import { ElementIconText } from '../services/get-element-icon-text.service';
 import { DataExplorerDashboardGridComponent } from './components/grid/data-explorer-dashboard-grid.component';
@@ -97,7 +102,12 @@ export const MY_NATIVE_FORMATS = {
     CoreUiModule,
     OwlDateTimeModule,
     OwlNativeDateTimeModule,
-    PlotlyViaWindowModule
+    PlotlyViaWindowModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatSliderModule,
+    MatChipsModule,
+    LabelingToolModule
   ],
   declarations: [
     DataExplorerV2Component,