You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2025/06/19 17:27:56 UTC

(streampipes) branch add-3d-time-series-chart created (now a7148715e5)

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

riemer pushed a change to branch add-3d-time-series-chart
in repository https://gitbox.apache.org/repos/asf/streampipes.git


      at a7148715e5 feat: Add 3D time-series chart

This branch includes the following new commits:

     new a7148715e5 feat: Add 3D time-series chart

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(streampipes) 01/01: feat: Add 3D time-series chart

Posted by ri...@apache.org.
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 <do...@gmail.com>
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'),