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,