This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch improve-data-explorer-widgets in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 789bc67a2278a5602a186be29fc930b226113f4c Author: Dominik Riemer <[email protected]> AuthorDate: Mon Aug 28 20:20:23 2023 +0200 Improve rendering of echarts widgets in data explorer --- ui/deployment/appng5.module.mst | 13 + ui/package-lock.json | 41 +++ ui/package.json | 3 + .../echarts-transform/histogram.transform.ts | 78 +++++ .../echarts-transform/round-values.transform.ts | 75 +++++ .../value-distribution.transform.ts | 79 +++++ ...ta-explorer-visualisation-settings.component.ts | 2 +- .../base-data-explorer-echarts-widget.directive.ts | 112 +++++++ .../base/base-data-explorer-widget.directive.ts | 16 +- .../echarts-widget.component.html} | 22 +- .../widgets/base/echarts-widget.component.scss | 6 + ...distribution-chart-widget-config.component.html | 75 ++++- .../distribution-chart-widget-config.component.ts | 29 +- .../distribution-chart-widget.component.html | 65 ----- .../distribution-chart-widget.component.ts | 195 ++----------- .../model/distribution-chart-widget.model.ts | 5 + .../value-heatmap/value-heatmap.component.ts | 188 ------------ .../widgets/heatmap/heatmap-widget.component.ts | 65 +---- .../time-series-chart-widget.component.html | 2 + .../time-series-chart-widget.component.ts | 4 + ui/src/app/data-explorer/data-explorer.module.ts | 2 - .../echarts-renderer/sp-base-echarts-renderer.ts | 324 +++++++++++++++++++++ .../sp-base-single-field-echarts-renderer.ts | 121 ++++++++ .../sp-heatmap-renderer.ts} | 20 ++ .../echarts-renderer/sp-histogram-renderer.ts | 78 +++++ .../echarts-renderer/sp-pie-renderer.ts | 100 +++++++ .../echarts-renderer/sp-value-heatmap-renderer.ts | 101 +++++++ .../dataset.model.ts} | 38 +++ .../models/dataview-dashboard.model.ts | 20 +- .../registry/data-explorer-widget-registry.ts | 8 +- .../registry/data-explorer-widgets.ts | 10 +- 31 files changed, 1393 insertions(+), 504 deletions(-) diff --git a/ui/deployment/appng5.module.mst b/ui/deployment/appng5.module.mst index 7751d0c59..ae5630a3b 100644 --- a/ui/deployment/appng5.module.mst +++ b/ui/deployment/appng5.module.mst @@ -54,6 +54,19 @@ import { AvailableRolesService } from './services/available-roles.service'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { ValueDistributionTransform } from './core-ui/echarts-transform/value-distribution.transform'; +import { HistogramTransform } from './core-ui/echarts-transform/histogram.transform'; +import { RoundValuesTransform} from './core-ui/echarts-transform/round-values.transform'; + +import * as echarts from 'echarts'; +import * as transform from 'echarts-simple-transform'; + +echarts.registerTransform(transform.aggregate); +echarts.registerTransform(ValueDistributionTransform); +echarts.registerTransform(HistogramTransform); +echarts.registerTransform(RoundValuesTransform); + + import * as $ from 'jquery'; @NgModule({ diff --git a/ui/package-lock.json b/ui/package-lock.json index ab368ab59..9131d78c9 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -38,10 +38,13 @@ "angular2-uuid": "1.1.1", "codemirror": "^5.65.11", "console-browserify": "^1.2.0", + "d3-array": "^3.2.4", "dagre": "0.8.5", "datatables.net": "^1.13.1", "datatables.net-dt": "^1.13.1", "echarts": "^5.4.1", + "echarts-simple-transform": "^1.0.0", + "echarts-stat": "^1.2.0", "echarts-wordcloud": "^2.1.0", "file-saver": "2.0.5", "jquery": "^3.7.0", @@ -11735,6 +11738,24 @@ "zrender": "5.4.1" } }, + "node_modules/echarts-simple-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/echarts-simple-transform/-/echarts-simple-transform-1.0.0.tgz", + "integrity": "sha512-zidekQFwV7s2rLUBVvum9d7kbhX2uoVWqDNYWv+MciEbjBzlhdvLDzciSgeBNe9bwEiqVfGC6NP99zjo3JXDIg==", + "dependencies": { + "tslib": "2.0.3" + } + }, + "node_modules/echarts-simple-transform/node_modules/tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, + "node_modules/echarts-stat": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/echarts-stat/-/echarts-stat-1.2.0.tgz", + "integrity": "sha512-zLd7Kgs+tuTSeaK0VQEMNmnMivEkhvHIk1gpBtLzpRerfcIQ+Bd5XudOMmtwpaTc1WDZbA7d1V//iiBccR46Qg==" + }, "node_modules/echarts-wordcloud": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz", @@ -36354,6 +36375,26 @@ } } }, + "echarts-simple-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/echarts-simple-transform/-/echarts-simple-transform-1.0.0.tgz", + "integrity": "sha512-zidekQFwV7s2rLUBVvum9d7kbhX2uoVWqDNYWv+MciEbjBzlhdvLDzciSgeBNe9bwEiqVfGC6NP99zjo3JXDIg==", + "requires": { + "tslib": "2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, + "echarts-stat": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/echarts-stat/-/echarts-stat-1.2.0.tgz", + "integrity": "sha512-zLd7Kgs+tuTSeaK0VQEMNmnMivEkhvHIk1gpBtLzpRerfcIQ+Bd5XudOMmtwpaTc1WDZbA7d1V//iiBccR46Qg==" + }, "echarts-wordcloud": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index 2e88b61cb..a5236546c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -60,11 +60,14 @@ "angular2-uuid": "1.1.1", "codemirror": "^5.65.11", "console-browserify": "^1.2.0", + "d3-array": "^3.2.4", "dagre": "0.8.5", "datatables.net": "^1.13.1", "datatables.net-dt": "^1.13.1", "echarts": "^5.4.1", "echarts-wordcloud": "^2.1.0", + "echarts-stat": "^1.2.0", + "echarts-simple-transform": "^1.0.0", "file-saver": "2.0.5", "jquery": "^3.7.0", "jquery-ui-dist": "1.13.2", diff --git a/ui/src/app/core-ui/echarts-transform/histogram.transform.ts b/ui/src/app/core-ui/echarts-transform/histogram.transform.ts new file mode 100644 index 000000000..ba5c9f086 --- /dev/null +++ b/ui/src/app/core-ui/echarts-transform/histogram.transform.ts @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + DataTransformOption, + ExternalDataTransform, + ExternalDataTransformResultItem, +} from 'echarts/types/src/data/helper/transform'; + +import { bin } from 'd3-array'; +import { OptionSourceDataArrayRows } from 'echarts/types/src/util/types'; + +export interface HistogramConfig extends DataTransformOption { + field: string; + autoBin: boolean; + numberOfBins: number; + autoDomain: boolean; + domainMin: number; + domainMax: number; +} + +export const HistogramTransform: ExternalDataTransform<HistogramConfig> = { + type: 'sp:histogram', + + transform: function ( + params, + ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] { + const upstream = params.upstream; + const clonedData = upstream.cloneRawData(); + const field = params.config['field']; + const autoBin = params.config['autoBin']; + const numberOfBins = +params.config['numberOfBins']; + const autoDomain = params.config['autoDomain']; + const domainMin = +params.config['domainMin']; + const domainMax = +params.config['domainMax']; + const dimension = upstream.getDimensionInfo(field); + const values = (clonedData as any).map(row => row[dimension.index]); + values.shift(); + + let bins = bin(); + if (!autoBin && numberOfBins) { + bins = bins.thresholds(numberOfBins); + } + if (!autoDomain && domainMin && domainMax) { + bins = bins.domain([domainMin, domainMax]); + } + const d3h = bins(values); + const source: OptionSourceDataArrayRows = []; + const hist: number[] = d3h.map(item => item.length); + const binEdges = d3h.map(item => item.x0); + if (d3h.length) { + binEdges.push(d3h.at(-1).x1); + } + hist.forEach((val, index) => { + source.push([binEdges[index] + '-' + binEdges[index + 1], val]); + }); + + return { + data: source, + dimensions: ['edge', 'hist'], + }; + }, +}; diff --git a/ui/src/app/core-ui/echarts-transform/round-values.transform.ts b/ui/src/app/core-ui/echarts-transform/round-values.transform.ts new file mode 100644 index 000000000..6c9cd3e5f --- /dev/null +++ b/ui/src/app/core-ui/echarts-transform/round-values.transform.ts @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + DataTransformOption, + ExternalDataTransform, + ExternalDataTransformResultItem, +} from 'echarts/types/src/data/helper/transform'; +import { DataExplorerField } from '@streampipes/platform-services'; +import { OptionSourceDataArrayRows } from 'echarts/types/src/util/types'; + +export interface RoundValuesConfig extends DataTransformOption { + fields: DataExplorerField[]; + roundingValue: number; +} + +export const RoundValuesTransform: ExternalDataTransform<RoundValuesConfig> = { + type: 'sp:round', + + transform: function ( + params, + ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] { + const fields: string[] = ( + params.config['fields'] as DataExplorerField[] + ) + .filter(f => f.fieldCharacteristics.numeric) + .map(f => f.fullDbName); + const roundingValue = +params.config['roundingValue']; + const upstream = params.upstream; + + const dimsDef = upstream.cloneAllDimensionInfo(); + + const roundColumnIndices = dimsDef + .filter(dim => fields.indexOf(dim.name) > -1) + .map(dim => dim.index); + + const data = upstream.cloneRawData() as OptionSourceDataArrayRows; + const result = data.map((row, index) => { + if (index == 0) { + return row; + } else { + return row.map((value, index) => { + if (roundColumnIndices.indexOf(index) > -1) { + return ( + Math.round((value as number) / roundingValue) * + roundingValue + ); + } else { + return value; + } + }); + } + }); + result.shift(); + return { + dimensions: upstream.cloneAllDimensionInfo(), + data: result, + }; + }, +}; diff --git a/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts b/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts new file mode 100644 index 000000000..bc88eecf5 --- /dev/null +++ b/ui/src/app/core-ui/echarts-transform/value-distribution.transform.ts @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + DataTransformOption, + ExternalDataTransform, + ExternalDataTransformResultItem, +} from 'echarts/types/src/data/helper/transform'; + +export interface ValueDistributionConfig extends DataTransformOption { + field: string; + resolution: number; +} + +export const ValueDistributionTransform: ExternalDataTransform<ValueDistributionConfig> = + { + type: 'sp:value-distribution', + + transform: function ( + params, + ): ExternalDataTransformResultItem | ExternalDataTransformResultItem[] { + const upstream = params.upstream; + const clonedData = upstream.cloneRawData(); + const field = params.config['field']; + const resolution = +params.config['resolution']; + const dimension = upstream.getDimensionInfo(field); + + const dataResult = []; + const allValues = (clonedData as any).map( + row => row[dimension.index], + ); + allValues.shift(); + const total = allValues.length; + let currentCount = 0; + allValues.sort((a, b) => a - b); + let start = allValues[0]; + for (let i = 0; i < allValues.length; i++) { + const value = allValues[i]; + if (value < start + +resolution) { + currentCount += 1; + } + if (value >= start + resolution || i + 1 === allValues.length) { + const currentRange = + '>' + + start.toFixed(2) + + (i + 1 < allValues.length + ? '<' + allValues[i + 1].toFixed(2) + : ''); + dataResult.push([ + currentRange, + 0, + (currentCount + 1) / total, + ]); + currentCount = 0; + start = allValues[i + 1]; + } + } + + return { + data: dataResult, + dimensions: ['category', 'group', field], + }; + }, + }; diff --git a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts b/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts index f509b3b97..52e95293c 100644 --- a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts +++ b/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts @@ -33,7 +33,7 @@ export class DataExplorerVisualisationSettingsComponent implements OnInit { constructor(private widgetTypeService: WidgetTypeService) {} - availableWidgets: IWidget[]; + availableWidgets: IWidget<any>[]; ngOnInit(): void { this.availableWidgets = diff --git a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts new file mode 100644 index 000000000..7bf1e1dcb --- /dev/null +++ b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-echarts-widget.directive.ts @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Directive, OnInit } from '@angular/core'; +import { + DataExplorerWidgetModel, + SpQueryResult, +} from '@streampipes/platform-services'; +import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model'; +import { BaseDataExplorerWidgetDirective } from './base-data-explorer-widget.directive'; +import { ECharts } from 'echarts/core'; +import { EChartsOption } from 'echarts'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Directive() +export abstract class BaseDataExplorerEchartsWidgetDirective< + T extends DataExplorerWidgetModel, + > + extends BaseDataExplorerWidgetDirective<T> + implements OnInit +{ + eChartsInstance: ECharts; + currentWidth: number; + currentHeight: number; + + option: EChartsOption; + + configReady = false; + latestData: SpQueryResult[]; + + renderSubject = new Subject<void>(); + renderSubjectSubscription: Subscription; + + ngOnInit(): void { + super.ngOnInit(); + this.initOptions(); + this.renderSubjectSubscription = this.renderSubject + .pipe(debounceTime(300)) + .subscribe(() => { + this.renderChartOptions(this.latestData); + }); + } + + beforeDataFetched() {} + + onDataReceived(spQueryResult: SpQueryResult[]) { + this.renderChartOptions(spQueryResult); + this.latestData = spQueryResult; + this.setShownComponents(false, true, false, false); + } + + onResize(width: number, height: number) { + this.currentWidth = width; + this.currentHeight = height; + this.configReady = true; + this.applySize(width, height); + if (this.latestData) { + this.renderSubject.next(); + } + } + + onChartInit(ec: ECharts) { + this.eChartsInstance = ec; + this.applySize(this.currentWidth, this.currentHeight); + this.initOptions(); + } + + applySize(width: number, height: number) { + if (this.eChartsInstance) { + this.eChartsInstance.resize({ width, height }); + } + } + + renderChartOptions(spQueryResult: SpQueryResult[]): void { + this.option = { + ...this.getRenderer().render( + spQueryResult, + this.dataExplorerWidget, + { width: this.currentWidth, height: this.currentHeight }, + ), + }; + } + + refreshView() { + this.renderSubject.next(); + } + + abstract getRenderer(): SpEchartsRenderer<T>; + + initOptions() {} + + public cleanupSubscriptions(): void { + super.cleanupSubscriptions(); + this.renderSubjectSubscription.unsubscribe(); + } +} diff --git a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts index cc197ac2c..daef0ac5c 100644 --- a/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts +++ b/ui/src/app/data-explorer/components/widgets/base/base-data-explorer-widget.directive.ts @@ -93,6 +93,9 @@ export abstract class BaseDataExplorerWidgetDirective< resizeSub: Subscription; timeSelectionSub: Subscription; + widthOffset: number; + heightOffset: number; + requestQueue$: Subject<Observable<SpQueryResult>[]> = new Subject< Observable<SpQueryResult>[] >(); @@ -107,8 +110,8 @@ export abstract class BaseDataExplorerWidgetDirective< ) {} ngOnInit(): void { - const heightOffset = this.gridMode ? 70 : 65; - const widthOffset = this.gridMode ? 10 : 10; + this.heightOffset = this.gridMode ? 70 : 65; + this.widthOffset = this.gridMode ? 10 : 10; this.showData = true; const sourceConfigs = this.dataExplorerWidget.dataConfig.sourceConfigs; this.fieldProvider = @@ -181,8 +184,9 @@ export abstract class BaseDataExplorerWidgetDirective< info => { if (info.gridsterItem.id === this.dataExplorerWidget._id) { this.onResize( - this.gridsterItemComponent.width - widthOffset, - this.gridsterItemComponent.height - heightOffset, + this.gridsterItemComponent.width - this.widthOffset, + this.gridsterItemComponent.height - + this.heightOffset, ); } }, @@ -197,8 +201,8 @@ export abstract class BaseDataExplorerWidgetDirective< ); this.updateData(); this.onResize( - this.gridsterItemComponent.width - widthOffset, - this.gridsterItemComponent.height - heightOffset, + this.gridsterItemComponent.width - this.widthOffset, + this.gridsterItemComponent.height - this.heightOffset, ); } diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html similarity index 64% rename from ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html rename to ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html index 4af8d0181..80b5ecc3a 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.html +++ b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.html @@ -16,13 +16,27 @@ ~ --> -<div fxFlex="100"> +<div class="main-panel"> + <sp-no-data-in-date-range + *ngIf="showNoDataInDateRange" + [viewDateRange]="timeSettings" + class="h-100" + > + </sp-no-data-in-date-range> + + <sp-too-much-data + [amountOfEvents]="amountOfTooMuchEvents" + (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()" + *ngIf="showTooMuchData" + class="h-100" + > + </sp-too-much-data> + <div echarts [options]="option" - [ngStyle]="{ width: currentWidth_, height: '100%' }" + [ngStyle]="{ width: currentWidth, height: '100%' }" (chartInit)="onChartInit($event)" - [merge]="dynamic" - *ngIf="configReady" + *ngIf="option" ></div> </div> diff --git a/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss new file mode 100644 index 000000000..43abc27d7 --- /dev/null +++ b/ui/src/app/data-explorer/components/widgets/base/echarts-widget.component.scss @@ -0,0 +1,6 @@ +.main-panel { + width: 100%; + height: 100%; + align-content: center; + display: block; +} diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html index b6707989c..cbb230145 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html +++ b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.html @@ -51,6 +51,75 @@ > </sp-select-property> + <div + fxLayoutGap="10px" + fxLayout="column" + *ngIf=" + currentlyConfiguredWidget.visualizationConfig.displayType === + 'histogram' + " + > + <mat-checkbox + [(ngModel)]="currentlyConfiguredWidget.visualizationConfig.autoBin" + (change)="triggerDataRefresh()" + > + Auto-compute number of bins + </mat-checkbox> + <mat-form-field + fxFlex="100" + color="accent" + appearance="outline" + *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoBin" + > + <mat-label><span>Number of bins</span></mat-label> + <input + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig.numberOfBins + " + matInput + (input)="onNumBinChange($event.target.value)" + /> + </mat-form-field> + <mat-checkbox + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig.autoDomain + " + (change)="triggerViewRefresh()" + > + All values + </mat-checkbox> + <mat-form-field + fxFlex="100" + color="accent" + appearance="outline" + *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoDomain" + > + <mat-label><span>Minimum value</span></mat-label> + <input + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig.domainMin + " + matInput + (input)="onDomainMinChange($event.target.value)" + /> + </mat-form-field> + <mat-form-field + fxFlex="100" + color="accent" + appearance="outline" + *ngIf="!currentlyConfiguredWidget.visualizationConfig.autoDomain" + > + <mat-label><span>Maximum value</span></mat-label> + <input + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig.domainMax + " + matInput + (input)="onDomainMaxChange($event.target.value)" + /> + </mat-form-field> + </div> + <h5 *ngIf=" currentlyConfiguredWidget.visualizationConfig.displayType === @@ -62,6 +131,7 @@ Rounding </h5> <mat-form-field + appearance="outline" class="marginColorField" color="accent" fxFlex="35" @@ -87,13 +157,14 @@ </mat-form-field> <div + class="mt-10" *ngIf=" currentlyConfiguredWidget.visualizationConfig.displayType === 'heatmap' " > - <mat-form-field fxFlex="100" color="accent"> - <mat-label> <span>Resolution</span></mat-label> + <mat-form-field fxFlex="100" color="accent" appearance="outline"> + <mat-label><span>Resolution</span></mat-label> <input [(ngModel)]=" currentlyConfiguredWidget.visualizationConfig.resolution diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts index 3c74771d1..579df93a5 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts +++ b/ui/src/app/data-explorer/components/widgets/distribution-chart/config/distribution-chart-widget-config.component.ts @@ -44,7 +44,7 @@ export class DistributionWidgetConfigComponent setSelectedProperty(field: DataExplorerField) { this.currentlyConfiguredWidget.visualizationConfig.selectedProperty = field; - this.triggerDataRefresh(); + this.triggerViewRefresh(); } updateDisplayType(selectedType: string) { @@ -61,19 +61,35 @@ export class DistributionWidgetConfigComponent this.currentlyConfiguredWidget.visualizationConfig.selectedProperty = this.fieldProvider.numericFields[0]; } - this.triggerDataRefresh(); + this.triggerViewRefresh(); } updateRoundingValue(selectedType: number) { this.currentlyConfiguredWidget.visualizationConfig.roundingValue = selectedType; - this.triggerDataRefresh(); + this.triggerViewRefresh(); } onResolutionChange(resolution: number): void { this.currentlyConfiguredWidget.visualizationConfig.resolution = resolution; - this.triggerDataRefresh(); + this.triggerViewRefresh(); + } + + onNumBinChange(numberOfBins: number): void { + this.currentlyConfiguredWidget.visualizationConfig.numberOfBins = + numberOfBins; + this.triggerViewRefresh(); + } + + onDomainMinChange(value: number) { + this.currentlyConfiguredWidget.visualizationConfig.domainMin = value; + this.triggerViewRefresh(); + } + + onDomainMaxChange(value: number) { + this.currentlyConfiguredWidget.visualizationConfig.domainMax = value; + this.triggerViewRefresh(); } protected getWidgetType(): WidgetType { @@ -87,6 +103,11 @@ export class DistributionWidgetConfigComponent displayType: 'histogram', roundingValue: 0.1, resolution: 1, + autoBin: true, + numberOfBins: 10, + domainMin: 0, + domainMax: 100, + autoDomain: true, }; } } diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html deleted file mode 100644 index 18f024579..000000000 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.html +++ /dev/null @@ -1,65 +0,0 @@ -<!-- - ~ Licensed to the Apache Software Foundation (ASF) under one or more - ~ contributor license agreements. See the NOTICE file distributed with - ~ this work for additional information regarding copyright ownership. - ~ The ASF licenses this file to You under the Apache License, Version 2.0 - ~ (the "License"); you may not use this file except in compliance with - ~ the License. You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - ~ - --> - -<div - fxLayout="column" - fxFlex="100" - [ngStyle]="{ - background: dataExplorerWidget.baseAppearanceConfig.backgroundColor, - color: dataExplorerWidget.baseAppearanceConfig.textColor - }" -> - <sp-no-data-in-date-range - *ngIf="showNoDataInDateRange" - [viewDateRange]="timeSettings" - class="h-100" - > - </sp-no-data-in-date-range> - - <sp-too-much-data - [amountOfEvents]="amountOfTooMuchEvents" - (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()" - *ngIf="showTooMuchData" - class="h-100" - ></sp-too-much-data> - - <plotly-plot - *ngIf=" - showData && - !(dataExplorerWidget.visualizationConfig.displayType === 'heatmap') - " - [divId]="dataExplorerWidget._id" - class="plotly-container" - [data]="data" - [layout]="graph.layout" - [config]="graph.config" - > - </plotly-plot> - - <sp-data-explorer-value-heatmap-widget - *ngIf=" - showData && - dataExplorerWidget.visualizationConfig.displayType === 'heatmap' - " - [currentWidth]="currentWidth" - [currentHeight]="currentHeight" - [widgetConfig]="dataExplorerWidget" - [data]="latestData" - > - </sp-data-explorer-value-heatmap-widget> -</div> diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts index eb9da4cac..98183b228 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts +++ b/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.ts @@ -17,192 +17,22 @@ */ import { Component, OnInit } from '@angular/core'; -import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive'; import { DistributionChartWidgetModel } from './model/distribution-chart-widget.model'; -import { - DataExplorerField, - SpQueryResult, -} from '@streampipes/platform-services'; +import { DataExplorerField } from '@streampipes/platform-services'; +import { BaseDataExplorerEchartsWidgetDirective } from '../base/base-data-explorer-echarts-widget.directive'; +import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model'; +import { DataExplorerWidgetRegistry } from '../../../registry/data-explorer-widget-registry'; +import { WidgetType } from '../../../registry/data-explorer-widgets'; @Component({ selector: 'sp-data-explorer-distribution-chart-widget', - templateUrl: './distribution-chart-widget.component.html', - styleUrls: ['./distribution-chart-widget.component.scss'], + templateUrl: '../base/echarts-widget.component.html', + styleUrls: ['../base/echarts-widget.component.scss'], }) export class DistributionChartWidgetComponent - extends BaseDataExplorerWidgetDirective<DistributionChartWidgetModel> + extends BaseDataExplorerEchartsWidgetDirective<DistributionChartWidgetModel> implements OnInit { - data = []; - latestData: SpQueryResult[]; - - rowNo = 2; - colNo = 2; - fixedColNo = 2; - - currentWidth: number; - currentHeight: number; - - graph = { - layout: { - grid: { - rows: this.rowNo, - columns: this.colNo, - }, - font: { - color: '#FFF', - }, - autosize: true, - plot_bgcolor: '#fff', - paper_bgcolor: '#fff', - }, - - config: { - modeBarButtonsToRemove: [ - 'lasso2d', - 'select2d', - 'toggleSpikelines', - 'toImage', - ], - displaylogo: false, - displayModeBar: false, - responsive: true, - }, - }; - - refreshView() { - this.updateAppearance(); - } - - transform(rows, index: number): any[] { - return rows.map(row => row[index]); - } - - prepareData(spQueryResult: SpQueryResult[]) { - this.data = []; - - const len = spQueryResult[0].allDataSeries.length; - - const even = len % this.colNo === 0; - - this.rowNo = even ? len / this.fixedColNo : (len + 1) / this.fixedColNo; - - this.colNo = len === 1 ? 1 : this.fixedColNo; - - let rowCount = 0; - let colCount = 0; - - spQueryResult[0].allDataSeries.map((group, gindex) => { - const series = group; - const finalLabels: string[] = []; - const finalValues: number[] = []; - const values: Map<string, number> = new Map(); - const field = - this.dataExplorerWidget.visualizationConfig.selectedProperty; - const index = this.getColumnIndex(field, spQueryResult[0]); - const histoValues: number[] = []; - - let groupName; - - if (group['tags'] != null) { - Object.entries(group['tags']).forEach(([key, val]) => { - groupName = val; - }); - } - - groupName = groupName === undefined ? field.fullDbName : groupName; - - if (series.total > 0) { - const colValues = this.transform(series.rows, index); - colValues.forEach(value => { - histoValues.push(value); - - if (field.fieldCharacteristics.numeric) { - const roundingValue = - this.dataExplorerWidget.visualizationConfig - .roundingValue; - value = - Math.round(value / roundingValue) * roundingValue; - } - - if (!values.has(value)) { - values.set(value, 0); - } - const currentVal = values.get(value); - values.set(value, currentVal + 1); - }); - } - values.forEach((value, key) => { - finalLabels.push(key); - finalValues.push(value); - }); - - let component; - - if ( - this.dataExplorerWidget.visualizationConfig.displayType === - 'pie' - ) { - component = { - name: groupName, - values: finalValues, - labels: finalLabels, - type: 'pie', - domain: { - row: rowCount, - column: colCount, - }, - }; - - if (colCount === this.colNo - 1) { - colCount = 0; - rowCount += 1; - } else { - colCount += 1; - } - } else { - component = { - x: histoValues, - type: 'histogram', - }; - } - this.data.push(component); - }); - } - - existsLabel(labels: string[], value: string) { - return labels.indexOf(value) > -1; - } - - updateAppearance() { - this.graph.layout.paper_bgcolor = - this.dataExplorerWidget.baseAppearanceConfig.backgroundColor; - this.graph.layout.plot_bgcolor = - this.dataExplorerWidget.baseAppearanceConfig.backgroundColor; - this.graph.layout.font.color = - this.dataExplorerWidget.baseAppearanceConfig.textColor; - this.graph.layout.grid = { - rows: this.rowNo, - columns: this.colNo, - }; - } - - onResize(width: number, height: number) { - this.currentWidth = width; - this.currentHeight = height; - this.graph.layout.autosize = false; - (this.graph.layout as any).width = width; - (this.graph.layout as any).height = height; - } - - beforeDataFetched() {} - - onDataReceived(spQueryResult: SpQueryResult[]) { - this.prepareData(spQueryResult); - this.latestData = spQueryResult; - this.setShownComponents(false, true, false, false); - } - handleUpdatedFields( addedFields: DataExplorerField[], removedFields: DataExplorerField[], @@ -228,4 +58,13 @@ export class DistributionChartWidgetComponent field => field.fieldCharacteristics.numeric, ); } + + getRenderer(): SpEchartsRenderer<DistributionChartWidgetModel> { + const widgetCategory = DataExplorerWidgetRegistry.getWidgetTemplate( + WidgetType.DistributionChart, + ); + const chartType = + this.dataExplorerWidget.visualizationConfig.displayType; + return widgetCategory.renderers.find(r => r.getType() === chartType); + } } diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts b/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts index e37cbf646..c7c7835bb 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts +++ b/ui/src/app/data-explorer/components/widgets/distribution-chart/model/distribution-chart-widget.model.ts @@ -28,6 +28,11 @@ export interface DistributionChartVisConfig extends DataExplorerVisConfig { displayType: string; roundingValue: number; resolution: number; + autoBin: boolean; + numberOfBins: number; + autoDomain: boolean; + domainMin: number; + domainMax: number; } export interface DistributionChartWidgetModel extends DataExplorerWidgetModel { diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts b/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts deleted file mode 100644 index 9dbdc824a..000000000 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Component, Input } from '@angular/core'; -import { EChartsOption } from 'echarts'; -import { ECharts } from 'echarts/core'; -import { SpQueryResult } from '@streampipes/platform-services'; -import { DistributionChartWidgetModel } from '../model/distribution-chart-widget.model'; - -@Component({ - selector: 'sp-data-explorer-value-heatmap-widget', - templateUrl: './value-heatmap.component.html', - styleUrls: ['./value-heatmap.component.scss'], -}) -export class SpValueHeatmapComponent { - currentWidth_: number; - currentHeight_: number; - data_: SpQueryResult[]; - - @Input() - widgetConfig: DistributionChartWidgetModel; - - configReady = true; - option: EChartsOption = { - grid: { - height: '80%', - top: '7%', - }, - tooltip: {}, - xAxis: { - type: 'category', - data: [], - }, - yAxis: { - type: 'category', - data: [], - }, - visualMap: { - min: 0, - max: 1, - calculable: true, - realtime: false, - top: '10px', - left: '10px', - inRange: { - color: [ - '#313695', - '#4575b4', - '#74add1', - '#abd9e9', - '#e0f3f8', - '#ffffbf', - '#fee090', - '#fdae61', - '#f46d43', - '#d73027', - '#a50026', - ], - }, - }, - series: [ - { - name: 'Gaussian', - type: 'heatmap', - data: [], - emphasis: { - itemStyle: { - borderColor: '#333', - borderWidth: 1, - }, - }, - animation: false, - }, - ], - }; - - dynamic: EChartsOption; - - eChartsInstance: ECharts; - - onChartInit(ec: ECharts) { - this.eChartsInstance = ec; - this.applySize(this.currentWidth_, this.currentHeight_); - this.initOptions(); - } - - initOptions() { - if (this.data_) { - const dataResult = []; - const resolution: number = - +this.widgetConfig.visualizationConfig.resolution; - const allRows = this.data_[0].allDataSeries[0].rows; - const fieldIndex = this.getFieldIndex( - this.data_[0].allDataSeries[0].headers, - ); - const allValues = allRows.map(row => row[fieldIndex]); - const total = allValues.length; - let currentCount = 0; - allValues.sort((a, b) => a - b); - let start = allValues[0]; - for (let i = 0; i < allValues.length; i++) { - const value = allValues[i]; - if (value < start + resolution) { - currentCount += 1; - } - if (value >= start + resolution || i + 1 === allValues.length) { - const currentRange = - '>' + - start.toFixed(2) + - (i + 1 < allValues.length - ? '<' + allValues[i + 1].toFixed(2) - : ''); - dataResult.push([ - currentRange, - 0, - (currentCount + 1) / total, - ]); - currentCount = 0; - start = allValues[i + 1]; - } - } - this.dynamic = this.option; - (this.dynamic.xAxis as any).data = dataResult.map(r => r[0]); - this.dynamic.series[0].data = dataResult; - this.dynamic.series[0].name = this.data_[0].headers[fieldIndex]; - if (this.eChartsInstance) { - this.eChartsInstance.setOption(this.dynamic); - } - } - } - - getFieldIndex(headers: string[]) { - return headers.indexOf( - this.widgetConfig.visualizationConfig.selectedProperty.fullDbName, - ); - } - - applySize(width: number, height: number) { - if (this.eChartsInstance) { - this.eChartsInstance.resize({ width, height }); - } - } - - @Input() - set currentWidth(currentWidth: number) { - this.currentWidth_ = currentWidth; - this.applySize(this.currentWidth_, this.currentHeight_); - } - - get currentWidth() { - return this.currentWidth_; - } - - @Input() - set currentHeight(currentHeight: number) { - this.currentHeight_ = currentHeight; - this.applySize(this.currentWidth_, this.currentHeight_); - } - - get currentHeight() { - return this.currentHeight_; - } - - @Input() - set data(data: SpQueryResult[]) { - this.data_ = data; - this.initOptions(); - } - - get data() { - return this.data_; - } -} diff --git a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts index d77497d8d..f6188d9db 100644 --- a/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts +++ b/ui/src/app/data-explorer/components/widgets/heatmap/heatmap-widget.component.ts @@ -22,52 +22,27 @@ import { SpQueryResult, } from '@streampipes/platform-services'; -import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive'; import { HeatmapWidgetModel } from './model/heatmap-widget.model'; import { EChartsOption } from 'echarts'; -import { ECharts } from 'echarts/core'; +import { BaseDataExplorerEchartsWidgetDirective } from '../base/base-data-explorer-echarts-widget.directive'; +import { SpEchartsRenderer } from '../../../models/dataview-dashboard.model'; @Component({ selector: 'sp-data-explorer-heatmap-widget', - templateUrl: './heatmap-widget.component.html', - styleUrls: ['./heatmap-widget.component.scss'], + templateUrl: '../base/echarts-widget.component.html', + styleUrls: ['../base/echarts-widget.component.scss'], }) export class HeatmapWidgetComponent - extends BaseDataExplorerWidgetDirective<HeatmapWidgetModel> + extends BaseDataExplorerEchartsWidgetDirective<HeatmapWidgetModel> implements OnInit { - eChartsInstance: ECharts; - currentWidth: number; - currentHeight: number; - - option = {}; dynamic: EChartsOption; - configReady = false; - ngOnInit(): void { super.ngOnInit(); - this.onSizeChanged( - this.gridsterItemComponent.width, - this.gridsterItemComponent.height, - ); - this.initOptions(); } - public refreshView() {} - - onResize(width: number, height: number) { - this.onSizeChanged(width, height); - } - - handleUpdatedFields( - addedFields: DataExplorerField[], - removedFields: DataExplorerField[], - ) {} - - beforeDataFetched() {} - onDataReceived(spQueryResult: SpQueryResult[]) { this.setShownComponents(false, true, false, false); const dataBundle = this.convertData(spQueryResult); @@ -82,25 +57,6 @@ export class HeatmapWidgetComponent } } - onChartInit(ec: ECharts) { - this.eChartsInstance = ec; - this.applySize(this.currentWidth, this.currentHeight); - this.initOptions(); - } - - protected onSizeChanged(width: number, height: number) { - this.currentWidth = width; - this.currentHeight = height; - this.configReady = true; - this.applySize(width, height); - } - - applySize(width: number, height: number) { - if (this.eChartsInstance) { - this.eChartsInstance.resize({ width, height }); - } - } - convertData(spQueryResult: SpQueryResult[]) { let min = 1000000; let max = -100000; @@ -325,4 +281,15 @@ export class HeatmapWidgetComponent } return num; } + + getRenderer(): SpEchartsRenderer<HeatmapWidgetModel> { + return undefined; + } + + protected handleUpdatedFields( + addedFields: DataExplorerField[], + removedFields: DataExplorerField[], + ) {} + + refreshView() {} } diff --git a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html index f4a9b8d5e..69cfdbcd3 100644 --- a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html +++ b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.html @@ -44,6 +44,8 @@ [divId]="dataExplorerWidget._id" class="plotly-container" [data]="data" + [updateOnlyWithRevision]="true" + [revision]="revision" [layout]="graph.layout" [config]="graph.config" > diff --git a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts index 1b36db696..00e2e95f5 100644 --- a/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts +++ b/ui/src/app/data-explorer/components/widgets/time-series-chart/time-series-chart-widget.component.ts @@ -103,6 +103,8 @@ export class TimeSeriesChartWidgetComponent }, }; + revision = 1; + ngOnInit(): void { this.updatemenus = [ { @@ -505,6 +507,7 @@ export class TimeSeriesChartWidgetComponent (this.graph.layout as any).width = width - this.offsetRightLineChart; (this.graph.layout as any).height = height; + this.revision += 1; }, 10); } @@ -524,6 +527,7 @@ export class TimeSeriesChartWidgetComponent ); this.setShownComponents(false, true, false, false); + this.revision += 1; } handleUpdatedFields( diff --git a/ui/src/app/data-explorer/data-explorer.module.ts b/ui/src/app/data-explorer/data-explorer.module.ts index fe6cfa40f..e902f19a3 100644 --- a/ui/src/app/data-explorer/data-explorer.module.ts +++ b/ui/src/app/data-explorer/data-explorer.module.ts @@ -96,7 +96,6 @@ import { WidgetDirective } from './components/widget/widget.directive'; import { CorrelationWidgetConfigComponent } from './components/widgets/correlation-chart/config/correlation-chart-widget-config.component'; import { TimeSelectionService } from './services/time-selection.service'; import { TooMuchDataComponent } from './components/widgets/utils/too-much-data/too-much-data.component'; -import { SpValueHeatmapComponent } from './components/widgets/distribution-chart/value-heatmap/value-heatmap.component'; import { RouterModule } from '@angular/router'; import { DataExplorerDashboardSlideViewComponent } from './components/widget-view/slide-view/data-explorer-dashboard-slide-view.component'; import { SharedUiModule } from '@streampipes/shared-ui'; @@ -210,7 +209,6 @@ export const MY_NATIVE_FORMATS = { SelectPropertiesComponent, SelectColorPropertiesComponent, SelectPropertyComponent, - SpValueHeatmapComponent, TableWidgetComponent, TableWidgetConfigComponent, MapWidgetConfigComponent, diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts new file mode 100644 index 000000000..f06aca6af --- /dev/null +++ b/ui/src/app/data-explorer/echarts-renderer/sp-base-echarts-renderer.ts @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + SpEchartsRenderer, + WidgetBaseAppearanceConfig, +} from '../models/dataview-dashboard.model'; +import { + DataExplorerWidgetModel, + SpQueryResult, +} from '@streampipes/platform-services'; +import { EChartsOption } from 'echarts'; +import { BoxLayoutOptionMixin } from 'echarts/types/src/util/types'; +import { + DatasetOption, + GridOption, + XAXisOption, + YAXisOption, +} from 'echarts/types/dist/shared'; +import { + AxisOptions, + GeneratedDataset, + GridOptions, + TagValue, + WidgetSize, +} from '../models/dataset.model'; +import { DataTransformOption } from 'echarts/types/src/data/helper/transform'; + +export abstract class SpBaseEchartsRenderer<T extends DataExplorerWidgetModel> + implements SpEchartsRenderer<T> +{ + minimumChartWidth = 200; + minimumChartHeight = 50; + gridMargin = 50; + + abstract getType(): string; + + render( + spQueryResult: SpQueryResult[], + widgetConfig: T, + widgetSize: WidgetSize, + ): EChartsOption { + const options = this.makeBaseConfig(); + const datasets = this.toDataset(spQueryResult, widgetConfig); + this.applyOptions(datasets, options, widgetConfig, widgetSize); + return options; + } + + makeBaseConfig(): EChartsOption { + return { + legend: { + orient: 'horizontal', + top: 'top', + }, + tooltip: { + show: true, + }, + }; + } + + applySeriesPosition( + series: BoxLayoutOptionMixin[], + gridOptions: GridOption[], + ) { + series.forEach((s, index) => { + const gridOption = gridOptions[index]; + s.left = gridOption.left; + s.top = gridOption.top; + s.width = gridOption.width; + s.height = gridOption.height; + }); + } + + makeAxisOptions( + appearanceConfig: WidgetBaseAppearanceConfig, + xAxisType: 'category' | 'value' | 'time' | 'log', + yAxisType: 'category' | 'value' | 'time' | 'log', + numberOfAxes: number, + ): AxisOptions { + const xAxisOptions: XAXisOption[] = []; + const yAxisOptions: YAXisOption[] = []; + for (let i = 0; i < numberOfAxes; i++) { + xAxisOptions.push( + this.makeAxis(xAxisType, i, appearanceConfig) as XAXisOption, + ); + yAxisOptions.push( + this.makeAxis(yAxisType, i, appearanceConfig) as YAXisOption, + ); + } + + return { + xAxisOptions: xAxisOptions, + yAxisOptions: yAxisOptions, + }; + } + + makeAxis( + axisType: 'category' | 'value' | 'time' | 'log', + gridIndex: number, + appearanceConfig: WidgetBaseAppearanceConfig, + ): XAXisOption | YAXisOption { + return { + type: axisType, + gridIndex: gridIndex, + nameTextStyle: { + color: appearanceConfig.textColor, + }, + axisTick: { + lineStyle: { + color: appearanceConfig.textColor, + }, + }, + axisLabel: { + color: appearanceConfig.textColor, + }, + axisLine: { + lineStyle: { + color: appearanceConfig.textColor, + }, + }, + }; + } + + makeSeriesName(tag: TagValue): string { + return tag.tagKeys.toString() + ' ' + tag.values.toString(); + } + + addSeriesTitles( + options: EChartsOption, + series: any[], + gridOptions: GridOption[], + ): void { + options.title = series.map((s, index) => { + const grid = gridOptions[index]; + return { + text: s.name, + textStyle: { + fontSize: 12, + fontWeight: 'normal', + }, + left: grid.left, + top: grid.top, + }; + }); + } + + makeGrid(tagLength: number, widgetSize: WidgetSize): GridOptions { + if (tagLength === 0) { + return { + grid: [ + { + height: widgetSize.height - 100, + width: widgetSize.width - 100, + top: this.gridMargin, + left: this.gridMargin, + right: this.gridMargin, + bottom: this.gridMargin, + }, + ], + numberOfColumns: 1, + numberOfRows: 1, + }; + } else { + const grid: GridOption[] = []; + const totalCanvasWidth = widgetSize.width - this.gridMargin * 2; + const totalCanvasHeight = widgetSize.height - this.gridMargin * 2; + const numberOfColumns = Math.max( + Math.floor(totalCanvasWidth / this.minimumChartWidth), + 1, + ); + const numberOfRows = Math.ceil(tagLength / numberOfColumns); + + const groupItemWidth = + (widgetSize.width - + (numberOfColumns * this.gridMargin + this.gridMargin)) / + numberOfColumns; + const groupItemHeight = Math.max( + this.minimumChartHeight, + (totalCanvasHeight - (numberOfRows - 1) * this.gridMargin) / + numberOfRows, + ); + + for (let i = 0; i < numberOfRows; i++) { + for (let j = 0; j < numberOfColumns; j++) { + if (i * j < tagLength) { + grid.push({ + left: Math.floor( + j * groupItemWidth + (j + 1) * this.gridMargin, + ), + top: + this.gridMargin + + i * (groupItemHeight + this.gridMargin), + width: groupItemWidth, + height: groupItemHeight, + borderWidth: 1, + }); + } + } + } + return { + grid: grid, + numberOfRows, + numberOfColumns, + }; + } + } + + toDataset( + queryResults: SpQueryResult[], + widgetConfig: T, + ): GeneratedDataset { + const datasets: DatasetOption[] = []; + const rawDataStartIndices: number[] = []; + const rawDataEndIndices: number[] = []; + const preparedDataStartIndices: number[] = []; + const preparedDataEndIndices: number[] = []; + const tagValues: TagValue[][] = []; + let initialTransformsCount = 0; + + queryResults.forEach((queryResult, index) => { + const currentDatasetSize = datasets.length; + const initialTransforms = this.initialTransforms( + widgetConfig, + index, + ); + const data = []; + data.push(queryResult.headers); + queryResult.allDataSeries.forEach(series => { + data.push(...series.rows); + }); + + const tags: TagValue[] = queryResult.allDataSeries + .map(series => series.tags) + .filter(tags => tags !== null) + .map(kv => { + return { + tagKeys: Object.keys(kv), + values: Object.values(kv), + }; + }); + + datasets.push({ + source: data, + dimensions: queryResult.headers, + }); + + if (initialTransforms.length > 0) { + initialTransformsCount++; + datasets.push({ + fromDatasetIndex: currentDatasetSize, + transform: initialTransforms, + }); + } + tags.forEach(tag => { + const filters = tag.tagKeys.map((key, index) => { + return { + dimension: key, + value: tag.values[index], + }; + }); + datasets.push({ + fromDatasetIndex: + currentDatasetSize + initialTransforms.length, + transform: { + type: 'filter', + config: { + and: filters, + }, + }, + }); + }); + tagValues.push(tags); + rawDataStartIndices.push( + currentDatasetSize + initialTransforms.length, + ); + rawDataEndIndices.push( + currentDatasetSize + initialTransforms.length + tags.length, + ); + }); + + return { + dataset: datasets, + tagValues: tagValues, + rawDataStartIndices: rawDataStartIndices, + rawDataEndIndices: rawDataEndIndices, + preparedDataStartIndices: preparedDataStartIndices, + preparedDataEndIndices: preparedDataEndIndices, + initialTransformsCount: initialTransformsCount, + }; + } + + makePercent(value: number) { + return value + '%'; + } + + abstract applyOptions( + datasets: GeneratedDataset, + options: EChartsOption, + widgetConfig: T, + widgetSize: WidgetSize, + ): void; + + initialTransforms( + widgetConfig: T, + sourceIndex: number, + ): DataTransformOption[] { + return []; + } +} diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts new file mode 100644 index 000000000..94236d83b --- /dev/null +++ b/ui/src/app/data-explorer/echarts-renderer/sp-base-single-field-echarts-renderer.ts @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + DataExplorerField, + DataExplorerWidgetModel, +} from '@streampipes/platform-services'; +import { EChartsOption, SeriesOption } from 'echarts'; +import { SpBaseEchartsRenderer } from './sp-base-echarts-renderer'; +import { GeneratedDataset, WidgetSize } from '../models/dataset.model'; +import { DataTransformOption } from 'echarts/types/src/data/helper/transform'; +import { WidgetBaseAppearanceConfig } from '../models/dataview-dashboard.model'; + +export abstract class SpBaseSingleFieldEchartsRenderer< + T extends DataExplorerWidgetModel, + S extends SeriesOption, +> extends SpBaseEchartsRenderer<T> { + applyOptions( + datasets: GeneratedDataset, + options: EChartsOption, + widgetConfig: T, + widgetSize: WidgetSize, + ) { + const affectedField = this.getAffectedField(widgetConfig); + const sourceIndex = affectedField.sourceIndex; + const tags = datasets.tagValues[sourceIndex]; + const rawDatasetStartIndex = datasets.rawDataStartIndices[sourceIndex]; + const numberOfCharts = tags.length === 0 ? 1 : tags.length; + const series = []; + const seriesStartIndex = this.computeSeriesStart(datasets); + + for (let i = 0; i < numberOfCharts; i++) { + datasets.dataset.push({ + fromDatasetIndex: rawDatasetStartIndex + i, + transform: this.addDatasetTransform(widgetConfig), + }); + const seriesName = + tags.length === 0 ? 'Default' : this.makeSeriesName(tags[i]); + series.push( + this.addSeriesItem( + seriesName, + seriesStartIndex + i, + widgetConfig, + i, + ), + ); + } + const gridOptions = this.makeGrid(tags.length, widgetSize); + options.grid = gridOptions.grid; + if (this.showAxes()) { + const a = this.makeAxisOptions( + widgetConfig.baseAppearanceConfig as WidgetBaseAppearanceConfig, + this.getXAxisType(), + this.getYAxisType(), + gridOptions.numberOfRows * gridOptions.numberOfColumns, + ); + options.xAxis = a.xAxisOptions; + options.yAxis = a.yAxisOptions; + } + if (this.shouldApplySeriesPosition()) { + this.applySeriesPosition(series, gridOptions.grid); + } + if (numberOfCharts > 1) { + this.addSeriesTitles(options, series, gridOptions.grid); + } + options.dataset = datasets.dataset; + options.series = series; + this.addAdditionalConfigs(options); + } + + showAxes(): boolean { + return true; + } + + abstract addDatasetTransform(widgetConfig: T): DataTransformOption; + + abstract addAdditionalConfigs(option: EChartsOption): void; + + abstract addSeriesItem( + name: string, + datasetIndex: number, + widgetConfig: T, + index: number, + ): S; + + abstract getAffectedField(widgetConfig: T): DataExplorerField; + + private computeSeriesStart(datasets: GeneratedDataset) { + return ( + datasets.rawDataEndIndices[datasets.rawDataEndIndices.length - 1] + + 1 + ); + } + + shouldApplySeriesPosition(): boolean { + return false; + } + + getXAxisType(): 'category' | 'value' | 'time' | 'log' { + return 'category'; + } + + getYAxisType(): 'category' | 'value' | 'time' | 'log' { + return 'category'; + } +} diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts similarity index 56% rename from ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss rename to ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts index 13cbc4aac..d069d63f5 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/value-heatmap/value-heatmap.component.scss +++ b/ui/src/app/data-explorer/echarts-renderer/sp-heatmap-renderer.ts @@ -15,3 +15,23 @@ * limitations under the License. * */ + +import { SpBaseEchartsRenderer } from './sp-base-echarts-renderer'; +import { HeatmapWidgetModel } from '../components/widgets/heatmap/model/heatmap-widget.model'; +import { GeneratedDataset, WidgetSize } from '../models/dataset.model'; +import { EChartsOption } from 'echarts'; + +export class SpHeatmapRenderer extends SpBaseEchartsRenderer<HeatmapWidgetModel> { + applyOptions( + datasets: GeneratedDataset, + options: EChartsOption, + widgetConfig: HeatmapWidgetModel, + widgetSize: WidgetSize, + ): void { + // TODO + } + + getType(): string { + return 'heatmap'; + } +} diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts new file mode 100644 index 000000000..12310a00d --- /dev/null +++ b/ui/src/app/data-explorer/echarts-renderer/sp-histogram-renderer.ts @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DistributionChartWidgetModel } from '../components/widgets/distribution-chart/model/distribution-chart-widget.model'; +import { BarSeriesOption, EChartsOption } from 'echarts'; +import { SpBaseSingleFieldEchartsRenderer } from './sp-base-single-field-echarts-renderer'; +import { DataTransformOption } from 'echarts/types/src/data/helper/transform'; + +export class SpHistogramRenderer extends SpBaseSingleFieldEchartsRenderer< + DistributionChartWidgetModel, + BarSeriesOption +> { + getType(): string { + return 'histogram'; + } + + addAdditionalConfigs(option: EChartsOption) { + //do nothing + } + + addDatasetTransform( + widgetConfig: DistributionChartWidgetModel, + ): DataTransformOption { + return { + type: 'sp:histogram', + config: { + field: widgetConfig.visualizationConfig.selectedProperty + .fullDbName, + autoBin: widgetConfig.visualizationConfig.autoBin, + numberOfBins: widgetConfig.visualizationConfig.numberOfBins, + autoDomain: widgetConfig.visualizationConfig.autoDomain, + domainMin: widgetConfig.visualizationConfig.domainMin, + domainMax: widgetConfig.visualizationConfig.domainMax, + }, + }; + } + + addSeriesItem( + name: string, + datasetIndex: number, + widgetConfig: DistributionChartWidgetModel, + index: number, + ): BarSeriesOption { + return { + name, + type: 'bar', + universalTransition: true, + datasetIndex: datasetIndex, + xAxisIndex: index, + yAxisIndex: index, + encode: { x: 'edge', y: 'hist' }, + barWidth: '99.3%', + }; + } + + getAffectedField(widgetConfig: DistributionChartWidgetModel) { + return widgetConfig.visualizationConfig.selectedProperty; + } + + getYAxisType(): 'value' | 'category' | 'time' | 'log' { + return 'value'; + } +} diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts new file mode 100644 index 000000000..1bae54ecd --- /dev/null +++ b/ui/src/app/data-explorer/echarts-renderer/sp-pie-renderer.ts @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DistributionChartWidgetModel } from '../components/widgets/distribution-chart/model/distribution-chart-widget.model'; +import { EChartsOption, PieSeriesOption } from 'echarts'; +import { DataTransformOption } from 'echarts/types/src/data/helper/transform'; +import { SpBaseSingleFieldEchartsRenderer } from './sp-base-single-field-echarts-renderer'; + +export class SpPieRenderer extends SpBaseSingleFieldEchartsRenderer< + DistributionChartWidgetModel, + PieSeriesOption +> { + getType(): string { + return 'pie'; + } + + addDatasetTransform( + widgetConfig: DistributionChartWidgetModel, + ): DataTransformOption { + const field = + widgetConfig.visualizationConfig.selectedProperty.fullDbName; + return { + type: 'ecSimpleTransform:aggregate', + config: { + resultDimensions: [ + { name: 'name', from: field }, + { name: 'value', from: 'time', method: 'count' }, + ], + groupBy: field, + }, + }; + } + + addAdditionalConfigs(option: EChartsOption) { + // do nothing + } + + addSeriesItem( + name: string, + datasetIndex: number, + widgetConfig: DistributionChartWidgetModel, + ): PieSeriesOption { + return { + name, + type: 'pie', + universalTransition: true, + datasetIndex: datasetIndex, + encode: { itemName: 'name', value: 'value' }, + }; + } + + initialTransforms( + widgetConfig: DistributionChartWidgetModel, + sourceIndex: number, + ): DataTransformOption[] { + const fieldSource = widgetConfig.visualizationConfig.selectedProperty; + return fieldSource.sourceIndex === sourceIndex && + fieldSource.fieldCharacteristics.numeric + ? [ + { + type: 'sp:round', + config: { + fields: [ + widgetConfig.visualizationConfig.selectedProperty, + ], + roundingValue: + widgetConfig.visualizationConfig.roundingValue, + }, + }, + ] + : []; + } + + getAffectedField(widgetConfig: DistributionChartWidgetModel) { + return widgetConfig.visualizationConfig.selectedProperty; + } + + showAxes(): boolean { + return false; + } + + shouldApplySeriesPosition(): boolean { + return true; + } +} diff --git a/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts b/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts new file mode 100644 index 000000000..87d19abd1 --- /dev/null +++ b/ui/src/app/data-explorer/echarts-renderer/sp-value-heatmap-renderer.ts @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DistributionChartWidgetModel } from '../components/widgets/distribution-chart/model/distribution-chart-widget.model'; +import { EChartsOption, HeatmapSeriesOption } from 'echarts'; +import { SpBaseSingleFieldEchartsRenderer } from './sp-base-single-field-echarts-renderer'; +import { DataTransformOption } from 'echarts/types/src/data/helper/transform'; + +export class SpValueHeatmapRenderer extends SpBaseSingleFieldEchartsRenderer< + DistributionChartWidgetModel, + HeatmapSeriesOption +> { + getType(): string { + return 'heatmap'; + } + + addDatasetTransform( + widgetConfig: DistributionChartWidgetModel, + ): DataTransformOption { + const field = + widgetConfig.visualizationConfig.selectedProperty.fullDbName; + return { + type: 'sp:value-distribution', + config: { + field, + resolution: widgetConfig.visualizationConfig.resolution, + }, + }; + } + + addAdditionalConfigs(options: EChartsOption) { + options.legend = { show: false }; + options.visualMap = { + min: 0, + max: 1, + calculable: true, + realtime: false, + top: '0px', + right: '50px', + orient: 'horizontal', + inRange: { + color: [ + '#313695', + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + }, + }; + } + + addSeriesItem( + name: string, + datasetIndex: number, + widgetConfig: DistributionChartWidgetModel, + index: number, + ): HeatmapSeriesOption { + return { + universalTransition: true, + animation: true, + name: name, + type: 'heatmap', + datasetIndex: datasetIndex, + encode: { itemId: 0, value: 2 }, + xAxisIndex: index, + yAxisIndex: index, + emphasis: { + itemStyle: { + borderColor: '#333', + borderWidth: 1, + }, + }, + }; + } + + getAffectedField(widgetConfig: DistributionChartWidgetModel) { + return widgetConfig.visualizationConfig.selectedProperty; + } +} diff --git a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss b/ui/src/app/data-explorer/models/dataset.model.ts similarity index 51% rename from ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss rename to ui/src/app/data-explorer/models/dataset.model.ts index 13cbc4aac..494d11e82 100644 --- a/ui/src/app/data-explorer/components/widgets/distribution-chart/distribution-chart-widget.component.scss +++ b/ui/src/app/data-explorer/models/dataset.model.ts @@ -15,3 +15,41 @@ * limitations under the License. * */ + +import { + DatasetOption, + GridOption, + XAXisOption, + YAXisOption, +} from 'echarts/types/dist/shared'; + +export interface WidgetSize { + width: number; + height: number; +} + +export interface TagValue { + tagKeys: string[]; + values: any[]; +} + +export interface GeneratedDataset { + dataset: DatasetOption[]; + tagValues: TagValue[][]; + rawDataStartIndices: number[]; + rawDataEndIndices: number[]; + preparedDataStartIndices: number[]; + preparedDataEndIndices: number[]; + initialTransformsCount: number; +} + +export interface GridOptions { + grid: GridOption[]; + numberOfRows: number; + numberOfColumns: number; +} + +export interface AxisOptions { + xAxisOptions: XAXisOption[]; + yAxisOptions: YAXisOption[]; +} diff --git a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts index be9dbd075..c3b414f44 100644 --- a/ui/src/app/data-explorer/models/dataview-dashboard.model.ts +++ b/ui/src/app/data-explorer/models/dataview-dashboard.model.ts @@ -18,15 +18,31 @@ import { GridsterConfig } from 'angular-gridster2'; import { WidgetType } from '../registry/data-explorer-widgets'; -import { DataExplorerField } from '@streampipes/platform-services'; +import { + DataExplorerField, + DataExplorerWidgetModel, + SpQueryResult, +} from '@streampipes/platform-services'; +import { EChartsOption } from 'echarts'; +import { WidgetSize } from './dataset.model'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IDataViewDashboardConfig extends GridsterConfig {} -export interface IWidget { +export interface SpEchartsRenderer<T extends DataExplorerWidgetModel> { + getType(): string; + render( + queryResult: SpQueryResult[], + widgetConfig: T, + widgetSize: WidgetSize, + ): EChartsOption; +} + +export interface IWidget<T extends DataExplorerWidgetModel> { id: string; label: string; componentClass: any; + renderers?: SpEchartsRenderer<T>[]; } export interface WidgetBaseAppearanceConfig { diff --git a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts index 132e894c3..5772bb2cd 100644 --- a/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts +++ b/ui/src/app/data-explorer/registry/data-explorer-widget-registry.ts @@ -17,10 +17,14 @@ */ import { IWidget } from '../models/dataview-dashboard.model'; -import { WidgetTypeMap } from './data-explorer-widgets'; +import { WidgetType, WidgetTypeMap } from './data-explorer-widgets'; export class DataExplorerWidgetRegistry { - static getAvailableWidgetTemplates(): IWidget[] { + static getAvailableWidgetTemplates(): IWidget<any>[] { return Array.from(WidgetTypeMap.values()); } + + static getWidgetTemplate(type: WidgetType) { + return WidgetTypeMap.get(type); + } } diff --git a/ui/src/app/data-explorer/registry/data-explorer-widgets.ts b/ui/src/app/data-explorer/registry/data-explorer-widgets.ts index 91489dc9d..0b27cf31a 100644 --- a/ui/src/app/data-explorer/registry/data-explorer-widgets.ts +++ b/ui/src/app/data-explorer/registry/data-explorer-widgets.ts @@ -25,6 +25,9 @@ import { ImageWidgetComponent } from '../components/widgets/image/image-widget.c import { IndicatorChartWidgetComponent } from '../components/widgets/indicator/indicator-chart-widget.component'; import { CorrelationChartWidgetComponent } from '../components/widgets/correlation-chart/correlation-chart-widget.component'; import { DistributionChartWidgetComponent } from '../components/widgets/distribution-chart/distribution-chart-widget.component'; +import { SpPieRenderer } from '../echarts-renderer/sp-pie-renderer'; +import { SpValueHeatmapRenderer } from '../echarts-renderer/sp-value-heatmap-renderer'; +import { SpHistogramRenderer } from '../echarts-renderer/sp-histogram-renderer'; export enum WidgetType { Table, @@ -37,7 +40,7 @@ export enum WidgetType { DistributionChart, } -export const WidgetTypeMap = new Map<number, IWidget>([ +export const WidgetTypeMap = new Map<number, IWidget<any>>([ [ WidgetType.Table, { @@ -96,6 +99,11 @@ export const WidgetTypeMap = new Map<number, IWidget>([ id: 'distribution-chart', label: 'Distribution', componentClass: DistributionChartWidgetComponent, + renderers: [ + new SpHistogramRenderer(), + new SpPieRenderer(), + new SpValueHeatmapRenderer(), + ], }, ], ]);
