This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch add-3d-time-series-chart in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit a7148715e54bd85fc91c3a9db9f81cab5e7b3523 Author: Dominik Riemer <[email protected]> AuthorDate: Thu Jun 19 19:27:28 2025 +0200 feat: Add 3D time-series chart --- ui/deployment/app.module.mst | 1 + ui/package-lock.json | 18 ++ ui/package.json | 1 + .../time-series-3d-widget-config.component.html | 77 +++++++ .../time-series-3d-widget-config.component.ts | 64 ++++++ .../model/time-series-3d-widget.model.ts | 35 +++ .../time-series-3d-widget.component.ts | 239 +++++++++++++++++++++ .../data-explorer-shared.module.ts | 4 + .../registry/data-explorer-chart-registry.ts | 14 ++ 9 files changed, 453 insertions(+) diff --git a/ui/deployment/app.module.mst b/ui/deployment/app.module.mst index 29748c1a12..c27d5a8d0f 100644 --- a/ui/deployment/app.module.mst +++ b/ui/deployment/app.module.mst @@ -57,6 +57,7 @@ import { MapTransform } from './core-ui/echarts-transform/map.transform'; import { MarkdownModule } from 'ngx-markdown'; import * as echarts from 'echarts'; +import 'echarts-gl'; import * as transform from 'echarts-simple-transform'; echarts.registerTransform(transform.aggregate); diff --git a/ui/package-lock.json b/ui/package-lock.json index 569e56ebc1..c63db6e709 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -38,6 +38,7 @@ "dagre": "0.8.5", "date-fns": "^3.6.0", "echarts": "^5.6.0", + "echarts-gl": "^2.0.9", "echarts-simple-transform": "^1.0.0", "file-saver": "2.0.5", "jquery": "^3.7.0", @@ -8783,6 +8784,11 @@ "node": ">=8" } }, + "node_modules/claygl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz", + "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -10731,6 +10737,18 @@ "zrender": "5.6.1" } }, + "node_modules/echarts-gl": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz", + "integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==", + "dependencies": { + "claygl": "^1.2.1", + "zrender": "^5.1.1" + }, + "peerDependencies": { + "echarts": "^5.1.2" + } + }, "node_modules/echarts-simple-transform": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/echarts-simple-transform/-/echarts-simple-transform-1.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index db299c42eb..cf9b3890be 100644 --- a/ui/package.json +++ b/ui/package.json @@ -59,6 +59,7 @@ "dagre": "0.8.5", "date-fns": "^3.6.0", "echarts": "^5.6.0", + "echarts-gl": "^2.0.9", "echarts-simple-transform": "^1.0.0", "file-saver": "2.0.5", "jquery": "^3.7.0", diff --git a/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.html b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.html new file mode 100644 index 0000000000..34742a06a9 --- /dev/null +++ b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.html @@ -0,0 +1,77 @@ +<!-- + ~ 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. + ~ + --> + +<sp-visualization-config-outer + [configurationValid]=" + currentlyConfiguredWidget.visualizationConfig.configurationValid + " +> + <sp-configuration-box [title]="'Chart Type' | translate"> + <mat-form-field appearance="outline" color="accent" class="w-100"> + <mat-select + [(value)]=" + currentlyConfiguredWidget.visualizationConfig.chartType + " + (selectionChange)="triggerViewRefresh()" + > + <mat-option [value]="'bar3D'" + >{{ 'Bar' | translate }} + </mat-option> + <mat-option [value]="'scatter3D'" + >{{ 'Scatter' | translate }} + </mat-option> + </mat-select> + </mat-form-field> + </sp-configuration-box> + <sp-configuration-box [title]="'Field' | translate"> + <sp-select-single-property-config + [availableProperties]="fieldProvider.numericFields" + [selectedProperty]=" + currentlyConfiguredWidget.visualizationConfig.selectedProperty + " + (changeSelectedProperty)="setSelectedProperty($event)" + > + </sp-select-single-property-config> + </sp-configuration-box> + @if ( + currentlyConfiguredWidget.visualizationConfig.chartType === 'scatter3D' + ) { + <sp-configuration-box [title]="'Settings' | translate"> + <div fxFlex="30" fxLayoutAlign="start center"> + <small>{{ 'Scatter width' | translate }}</small> + </div> + <div fxFlex="70" fxLayoutAlign="start center"> + <mat-form-field + appearance="outline" + color="accent" + class="w-100" + > + <input + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig + .symbolSize + " + matInput + type="number" + (ngModelChange)="triggerViewRefresh()" + /> + </mat-form-field> + </div> + </sp-configuration-box> + } +</sp-visualization-config-outer> diff --git a/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.ts b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.ts new file mode 100644 index 0000000000..3a089ea2d8 --- /dev/null +++ b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/config/time-series-3d-widget-config.component.ts @@ -0,0 +1,64 @@ +/* + * 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 } from '@angular/core'; +import { BaseWidgetConfig } from '../../base/base-widget-config'; +import { ChartConfigurationService } from '../../../../services/chart-configuration.service'; +import { DataExplorerFieldProviderService } from '../../../../services/data-explorer-field-provider-service'; +import { + TimeSeries3dVisConfig, + TimeSeries3dWidgetModel, +} from '../model/time-series-3d-widget.model'; +import { DataExplorerField } from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-data-explorer-time-series-3d-widget-config', + templateUrl: './time-series-3d-widget-config.component.html', + standalone: false, +}) +export class TimeSeries3dWidgetConfigComponent extends BaseWidgetConfig< + TimeSeries3dWidgetModel, + TimeSeries3dVisConfig +> { + constructor( + widgetConfigurationService: ChartConfigurationService, + fieldService: DataExplorerFieldProviderService, + ) { + super(widgetConfigurationService, fieldService); + } + + setSelectedProperty(field: DataExplorerField) { + this.currentlyConfiguredWidget.visualizationConfig.selectedProperty = + field; + this.triggerViewRefresh(); + } + + protected applyWidgetConfig(config: TimeSeries3dVisConfig): void { + config.selectedProperty = this.fieldService.getSelectedField( + config.selectedProperty, + this.fieldProvider.numericFields, + () => this.fieldProvider.numericFields[0], + ); + config.chartType ??= 'bar3D'; + config.symbolSize ??= 0.5; + } + + protected requiredFieldsForChartPresent(): boolean { + return this.fieldProvider.numericFields?.length > 0; + } +} diff --git a/ui/src/app/data-explorer-shared/components/charts/time-series-3d/model/time-series-3d-widget.model.ts b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/model/time-series-3d-widget.model.ts new file mode 100644 index 0000000000..28751c2617 --- /dev/null +++ b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/model/time-series-3d-widget.model.ts @@ -0,0 +1,35 @@ +/* + * 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 { DataExplorerVisConfig } from '../../../../models/dataview-dashboard.model'; +import { + DataExplorerDataConfig, + DataExplorerField, + DataExplorerWidgetModel, +} from '@streampipes/platform-services'; + +export interface TimeSeries3dVisConfig extends DataExplorerVisConfig { + selectedProperty: DataExplorerField; + chartType: 'scatter3D' | 'bar3D'; + symbolSize: number; +} + +export interface TimeSeries3dWidgetModel extends DataExplorerWidgetModel { + dataConfig: DataExplorerDataConfig; + visualizationConfig: TimeSeries3dVisConfig; +} diff --git a/ui/src/app/data-explorer-shared/components/charts/time-series-3d/time-series-3d-widget.component.ts b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/time-series-3d-widget.component.ts new file mode 100644 index 0000000000..d6b8e4c3bc --- /dev/null +++ b/ui/src/app/data-explorer-shared/components/charts/time-series-3d/time-series-3d-widget.component.ts @@ -0,0 +1,239 @@ +/* + * 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, inject, OnDestroy, OnInit } from '@angular/core'; +import { + DataExplorerField, + SpQueryResult, +} from '@streampipes/platform-services'; +import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive'; +import { ECharts } from 'echarts/core'; +import { EChartsOption } from 'echarts'; +import { Subject, Subscription } from 'rxjs'; +import { ResizeEchartsService } from '../../../services/resize-echarts.service'; +import { debounceTime } from 'rxjs/operators'; +import { TimeSeries3dWidgetModel } from './model/time-series-3d-widget.model'; +import { EchartsBasicOptionsGeneratorService } from '../../../echarts-renderer/echarts-basic-options-generator.service'; +import { WidgetEchartsAppearanceConfig } from '../../../models/dataview-dashboard.model'; + +@Component({ + selector: 'sp-data-explorer-time-series-3d-widget', + templateUrl: '../base/echarts-widget.component.html', + standalone: false, +}) +export class TimeSeries3dWidgetComponent + extends BaseDataExplorerWidgetDirective<TimeSeries3dWidgetModel> + implements OnInit, OnDestroy +{ + eChartsInstance: ECharts; + currentWidth: number; + currentHeight: number; + + option: EChartsOption; + + configReady = false; + latestData: SpQueryResult[]; + + renderSubject = new Subject<void>(); + renderSubject$: Subscription; + resizeEcharts$: Subscription; + + private resizeEchartsService = inject(ResizeEchartsService); + private echartsBasicOptionsGeneratorService = inject( + EchartsBasicOptionsGeneratorService, + ); + + widgetTypeLabel: string; + + ngOnInit(): void { + super.ngOnInit(); + this.resizeEcharts$ = + this.resizeEchartsService.echartsResizeSubject.subscribe(width => { + this.currentWidth = width - this.widthOffset; + this.applySize(this.currentWidth, this.currentHeight); + this.refreshView(); + }); + this.renderSubject$ = this.renderSubject + .pipe(debounceTime(300)) + .subscribe(() => { + this.renderChartOptions(this.latestData); + }); + this.widgetTypeLabel = this.widgetRegistryService.getChartTemplate( + this.dataExplorerWidget.widgetType, + ).label; + } + + 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); + } + + applySize(width: number, height: number) { + if (this.eChartsInstance) { + this.eChartsInstance.resize({ width, height }); + } + } + + renderChartOptions(spQueryResult: SpQueryResult[]): void { + const chartData = this.convertChartData(spQueryResult); + if ( + this.dataExplorerWidget.visualizationConfig.configurationValid === + undefined || + this.dataExplorerWidget.visualizationConfig.configurationValid === + true + ) { + this.showInvalidConfiguration = false; + this.option = { + tooltip: {}, + visualMap: { + show: true, + min: chartData.min, + max: chartData.max, + top: '0px', + right: '50px', + orient: 'horizontal', + dimension: 2, + inRange: { + color: [ + '#313695', + '#4575b4', + '#74add1', + '#abd9e9', + '#e0f3f8', + '#ffffbf', + '#fee090', + '#fdae61', + '#f46d43', + '#d73027', + '#a50026', + ], + }, + }, + xAxis3D: { + type: 'time', + name: 'Time', + axisLabel: { + formatter: v => { + return new Date(v).toLocaleString(); + }, + }, + }, + yAxis3D: { + type: 'category', + name: 'Category', + data: chartData.yAxisLabels, + }, + zAxis3D: { + type: 'value', + name: this.dataExplorerWidget.visualizationConfig + .selectedProperty.runtimeName, + }, + grid3D: { + viewControl: { + projection: 'perspective', + }, + }, + series: [ + { + type: this.dataExplorerWidget.visualizationConfig + .chartType, + symbolSize: + this.dataExplorerWidget.visualizationConfig + .symbolSize, + wireframe: { + show: false, + }, + data: chartData.chartData, + } as any, + ], + }; + const baseConfig = + this.echartsBasicOptionsGeneratorService.makeBaseConfig( + this.dataExplorerWidget + .baseAppearanceConfig as WidgetEchartsAppearanceConfig, + {}, + ); + this.option = Object.assign(this.option, baseConfig); + } else { + this.showInvalidConfiguration = true; + } + } + + refreshView() { + this.renderSubject.next(); + } + + handleUpdatedFields( + addedFields: DataExplorerField[], + removedFields: DataExplorerField[], + ) {} + + convertChartData(input: any[]) { + const allSeries = input?.[0]?.allDataSeries ?? []; + const yAxisLabels: string[] = []; + const chartData: [number, number, number][] = []; + + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + + allSeries.forEach((series, sensorIdx) => { + const tagString = Object.entries(series.tags || {}) + .map(([k, v]) => `${k}=${v}`) + .join(', '); + yAxisLabels.push(tagString || `Sensor ${sensorIdx + 1}`); + + series.rows.forEach(row => { + const [timestamp, distance] = row; + chartData.push([timestamp, sensorIdx, distance]); + if (distance < min) min = distance; + if (distance > max) max = distance; + }); + }); + + return { + chartData, + yAxisLabels, + min, + max, + }; + } + + ngOnDestroy(): void { + this.cleanupSubscriptions(); + this.resizeEcharts$?.unsubscribe(); + this.renderSubject$?.unsubscribe(); + } +} diff --git a/ui/src/app/data-explorer-shared/data-explorer-shared.module.ts b/ui/src/app/data-explorer-shared/data-explorer-shared.module.ts index f51ced9b9e..024c69f322 100644 --- a/ui/src/app/data-explorer-shared/data-explorer-shared.module.ts +++ b/ui/src/app/data-explorer-shared/data-explorer-shared.module.ts @@ -98,6 +98,8 @@ import { SpTimeSeriesAppearanceConfigComponent } from './components/charts/time- import { SpDataZoomConfigComponent } from './components/chart-config/data-zoom-config/data-zoom-config.component'; import { TranslateModule } from '@ngx-translate/core'; import { ColorMappingOptionsConfigComponent } from './components/chart-config/color-mapping-options-config/color-mapping-options-config.component'; +import { TimeSeries3dWidgetConfigComponent } from './components/charts/time-series-3d/config/time-series-3d-widget-config.component'; +import { TimeSeries3dWidgetComponent } from './components/charts/time-series-3d/time-series-3d-widget.component'; @NgModule({ imports: [ @@ -186,6 +188,8 @@ import { ColorMappingOptionsConfigComponent } from './components/chart-config/co SpTimeSeriesAppearanceConfigComponent, SpDataZoomConfigComponent, ColorMappingOptionsConfigComponent, + TimeSeries3dWidgetConfigComponent, + TimeSeries3dWidgetComponent, ], exports: [DataExplorerChartContainerComponent], }) diff --git a/ui/src/app/data-explorer-shared/registry/data-explorer-chart-registry.ts b/ui/src/app/data-explorer-shared/registry/data-explorer-chart-registry.ts index 31d25983f2..ca2a778873 100644 --- a/ui/src/app/data-explorer-shared/registry/data-explorer-chart-registry.ts +++ b/ui/src/app/data-explorer-shared/registry/data-explorer-chart-registry.ts @@ -60,6 +60,8 @@ import { TrafficLightWidgetComponent } from '../components/charts/traffic-light/ import { StatusWidgetConfigComponent } from '../components/charts/status/config/status-widget-config.component'; import { StatusWidgetComponent } from '../components/charts/status/status-widget.component'; import { TranslateService } from '@ngx-translate/core'; +import { TimeSeries3dWidgetConfigComponent } from '../components/charts/time-series-3d/config/time-series-3d-widget-config.component'; +import { TimeSeries3dWidgetComponent } from '../components/charts/time-series-3d/time-series-3d-widget.component'; @Injectable({ providedIn: 'root' }) export class DataExplorerChartRegistry { @@ -160,6 +162,18 @@ export class DataExplorerChartRegistry { 'A heatmap that lets you map specific values to a color', ), }, + { + id: 'time-series-3d', + label: '3D Time-Series Chart', + widgetAppearanceConfigurationComponent: + SpEchartsWidgetAppearanceConfigComponent, + widgetConfigurationComponent: TimeSeries3dWidgetConfigComponent, + widgetComponent: TimeSeries3dWidgetComponent, + icon: 'view_column', + description: this.translateService.instant( + 'A 3D time-series chart chart that renders each dimension in its own space', + ), + }, { id: 'time-series-chart', label: this.translateService.instant('Time Series Chart'),
