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

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 23396697cc Add status heatmap and create color mapping component 
(#3516)
23396697cc is described below

commit 23396697cc2680aa0a21bd22251a25366d6d6628
Author: Marcel Früholz <[email protected]>
AuthorDate: Fri Mar 7 09:00:41 2025 +0100

    Add status heatmap and create color mapping component (#3516)
---
 .../color-mapping-options-config.component.html    | 154 +++++++++++++++++
 .../color-mapping-options-config.component.ts      | 148 ++++++++++++++++
 .../config/pie-chart-widget-config.component.html  |  93 +---------
 .../config/pie-chart-widget-config.component.ts    |  59 +------
 .../charts/pie/model/pie-chart-widget.model.ts     |   3 +-
 .../components/charts/pie/pie-renderer.service.ts  |  39 ++++-
 .../status-heatmap-widget-config.component.html    |  48 ++++++
 .../status-heatmap-widget-config.component.ts      |  68 ++++++++
 .../model/status-heatmap-widget.model.ts}          |  15 +-
 .../status-heatmap-renderer.service.ts             | 192 +++++++++++++++++++++
 .../data-explorer-shared.module.ts                 |   4 +
 .../base-single-field-echarts-renderer.ts          |  16 +-
 .../registry/data-explorer-chart-registry.ts       |  15 ++
 .../services/color-mapping.service.ts              |  11 +-
 14 files changed, 709 insertions(+), 156 deletions(-)

diff --git 
a/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
 
b/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
new file mode 100644
index 0000000000..5ed6cc3235
--- /dev/null
+++ 
b/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
@@ -0,0 +1,154 @@
+<!--
+  ~ 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" fxLayoutGap="10px">
+    <div
+        fxLayout="row"
+        fxLayoutGap="10px"
+        fxLayoutAlign="start center"
+        fxFlex="1"
+        class="checkbox-container"
+    >
+        <mat-checkbox
+            color="accent"
+            [(ngModel)]="showCustomColorMapping"
+            (ngModelChange)="setCustomColorMapping($event)"
+        >
+        </mat-checkbox>
+        <small>{{ 'Add custom color mapping' | translate }}</small>
+    </div>
+
+    <div *ngIf="!isSelectedPropertyBoolean && showCustomColorMapping">
+        <div style="margin-bottom: 20px">
+            <button mat-raised-button color="accent" (click)="addMapping()">
+                <i class="material-icons">add</i>
+                <span>&nbsp;{{ 'Add Mapping' | translate }}</span>
+            </button>
+        </div>
+
+        <div fxLayout="column" fxLayoutGap="10px">
+            <div
+                *ngFor="let mapping of colorMapping; let i = index"
+                fxLayout="row"
+                fxLayoutGap="10px"
+                fxLayoutAlign="start center"
+                fxFlex="1"
+                style="margin-top: 10px; align-items: center"
+            >
+                <div fxFlex="150px">
+                    <mat-form-field
+                        class="w-100"
+                        color="accent"
+                        appearance="outline"
+                    >
+                        <mat-label>{{ 'Value' | translate }}</mat-label>
+                        <input
+                            matInput
+                            [(ngModel)]="mapping.value"
+                            (ngModelChange)="updateMapping()"
+                        />
+                    </mat-form-field>
+                </div>
+                <div fxFlex="150px">
+                    <mat-form-field
+                        class="w-100"
+                        color="accent"
+                        appearance="outline"
+                    >
+                        <mat-label>{{ 'Label' | translate }}</mat-label>
+                        <input
+                            matInput
+                            [(ngModel)]="mapping.label"
+                            (ngModelChange)="updateMapping()"
+                        />
+                    </mat-form-field>
+                </div>
+                <div fxFlex="40px">
+                    <input
+                        [(colorPicker)]="mapping.color"
+                        [style.background]="mapping.color"
+                        style="
+                            height: 50%;
+                            width: 100%;
+                            border: none;
+                            border-radius: 10%;
+                            cursor: pointer;
+                        "
+                        (colorPickerChange)="updateColor(i, $event)"
+                    />
+                </div>
+                <div fxLayoutAlign="end center">
+                    <button
+                        mat-icon-button
+                        [matTooltip]="'Remove Mapping' | translate"
+                        color="accent"
+                        (click)="removeMapping(i)"
+                    >
+                        <i class="material-icons">delete</i>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div *ngIf="isSelectedPropertyBoolean && showCustomColorMapping">
+        <div
+            fxLayout="column"
+            fxLayoutGap="10px"
+            style="margin-top: 10px; margin-right: 10px"
+        >
+            <div
+                *ngFor="let mapping of colorMapping; let i = index"
+                fxLayout="row"
+                fxLayoutGap="10px"
+                fxLayoutAlign="start center"
+                fxFlex="1"
+                style="margin-top: 10px; align-items: center"
+            >
+                <div fxFlex="40px">{{ mapping.value }}</div>
+                <div fxFlex>
+                    <mat-form-field
+                        class="w-100"
+                        color="accent"
+                        appearance="outline"
+                    >
+                        <mat-label>Label</mat-label>
+                        <input
+                            matInput
+                            [(ngModel)]="mapping.label"
+                            (ngModelChange)="updateMapping()"
+                        />
+                    </mat-form-field>
+                </div>
+                <div fxFlex="70px">
+                    <input
+                        [(colorPicker)]="mapping.color"
+                        [style.background]="mapping.color"
+                        style="
+                            height: 50%;
+                            width: 100%;
+                            border: none;
+                            border-radius: 10%;
+                            cursor: pointer;
+                        "
+                        (colorPickerChange)="updateColor(i, $event)"
+                    />
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git 
a/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
 
b/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
new file mode 100644
index 0000000000..f31dececc7
--- /dev/null
+++ 
b/ui/src/app/data-explorer-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
@@ -0,0 +1,148 @@
+/*
+ * 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,
+    Output,
+    EventEmitter,
+    OnInit,
+    OnChanges,
+    SimpleChanges,
+} from '@angular/core';
+import { ColorMappingService } from '../../../services/color-mapping.service';
+import { DataExplorerField } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-color-mapping-options-config',
+    templateUrl: './color-mapping-options-config.component.html',
+})
+export class ColorMappingOptionsConfigComponent implements OnInit, OnChanges {
+    @Input() colorMapping: { value: string; label: string; color: string }[];
+
+    @Input() selectedProperty: DataExplorerField;
+
+    @Output()
+    viewRefreshEmitter: EventEmitter<void> = new EventEmitter<void>();
+
+    @Output()
+    colorMappingChange: EventEmitter<
+        { value: string; label: string; color: string }[]
+    > = new EventEmitter();
+
+    protected isSelectedPropertyBoolean: boolean;
+    protected showCustomColorMapping: boolean;
+    private wasPreviousFieldBoolean: boolean;
+
+    constructor(private colorMappingService: ColorMappingService) {}
+
+    ngOnInit(): void {
+        this.isSelectedPropertyBoolean = this.isBooleanPropertySelected();
+        this.showCustomColorMapping ??= false;
+        this.resetColorMappings();
+    }
+
+    ngOnChanges(changes: SimpleChanges): void {
+        if (
+            changes['selectedProperty'] &&
+            !changes['selectedProperty'].firstChange
+        ) {
+            this.resetColorMappings();
+            this.isSelectedPropertyBoolean = this.isBooleanPropertySelected();
+        }
+    }
+
+    resetColorMappings(): void {
+        const isNowBoolean = this.isBooleanPropertySelected();
+
+        if (!this.showCustomColorMapping) {
+            if (isNowBoolean) {
+                this.colorMapping = [
+                    { value: 'true', label: '', color: '#66BB66' },
+                    { value: 'false', label: '', color: '#BB6666' },
+                ];
+            } else {
+                this.colorMapping = [];
+            }
+        }
+        if (isNowBoolean) {
+            if (
+                !(this.colorMapping ?? []).some(
+                    mapping =>
+                        mapping.value === 'true' || mapping.value === 'false',
+                )
+            ) {
+                this.colorMapping = [
+                    { value: 'true', label: '', color: '#66BB66' },
+                    { value: 'false', label: '', color: '#BB6666' },
+                ];
+            }
+        } else {
+            if (this.wasPreviousFieldBoolean) {
+                this.colorMapping = [];
+            }
+        }
+        this.wasPreviousFieldBoolean = isNowBoolean;
+        this.colorMappingChange.emit(this.colorMapping);
+        this.viewRefreshEmitter.emit();
+    }
+
+    addMapping() {
+        this.colorMappingService.addMapping(this.colorMapping);
+        this.colorMappingChange.emit(this.colorMapping);
+        this.viewRefreshEmitter.emit();
+    }
+
+    removeMapping(index: number) {
+        this.colorMapping = this.colorMappingService.removeMapping(
+            this.colorMapping,
+            index,
+        );
+        this.colorMappingChange.emit(this.colorMapping);
+        this.viewRefreshEmitter.emit();
+    }
+
+    updateColor(index: number, newColor: string) {
+        this.colorMappingService.updateColor(
+            this.colorMapping,
+            index,
+            newColor,
+        );
+        this.colorMappingChange.emit(this.colorMapping);
+        this.viewRefreshEmitter.emit();
+    }
+
+    updateMapping() {
+        this.colorMappingChange.emit(this.colorMapping);
+        this.viewRefreshEmitter.emit();
+    }
+
+    isBooleanPropertySelected(): boolean {
+        return this.selectedProperty.fieldCharacteristics.binary;
+    }
+
+    setCustomColorMapping(showCustomColorMapping: boolean) {
+        this.showCustomColorMapping = showCustomColorMapping;
+
+        if (!showCustomColorMapping) {
+            this.resetColorMappings();
+        }
+
+        this.viewRefreshEmitter.emit();
+    }
+}
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.html
 
b/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.html
index f0fa2e4eef..2fc82d5913 100644
--- 
a/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.html
+++ 
b/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.html
@@ -87,98 +87,19 @@
                 </mat-slider>
                 <small>{{ slider.value }}% </small>
             </div>
-            <div
-                fxLayout="row"
-                fxLayoutGap="10px"
-                fxLayoutAlign="start center"
-                fxFlex="100"
-                class="checkbox-container"
-            >
-                <mat-checkbox
-                    color="accent"
-                    [(ngModel)]="
-                        currentlyConfiguredWidget.visualizationConfig
-                            .showCustomColorMapping
-                    "
-                    (ngModelChange)="showCustomColorMapping($event)"
-                >
-                </mat-checkbox>
-                <small>{{ 'Add custom color mapping' | translate }}</small>
-            </div>
 
-            <div
-                *ngIf="
+            <sp-color-mapping-options-config
+                [(colorMapping)]="
                     currentlyConfiguredWidget.visualizationConfig
-                        .showCustomColorMapping
+                        .colorMappingsPieChart
                 "
-            >
-                <button mat-raised-button color="accent" 
(click)="addMapping()">
-                    <i class="material-icons">add</i
-                    ><span>&nbsp;{{ 'Add Mapping' | translate }}</span>
-                </button>
-            </div>
-
-            <div
-                *ngIf="
+                [selectedProperty]="
                     currentlyConfiguredWidget.visualizationConfig
-                        .showCustomColorMapping
+                        .selectedProperty
                 "
+                (viewRefreshEmitter)="triggerViewUpdate()"
             >
-                <div fxLayout="column" fxLayoutGap="10px">
-                    <div
-                        *ngFor="
-                            let mapping of currentlyConfiguredWidget
-                                .visualizationConfig.colorMappings;
-                            let i = index
-                        "
-                        fxLayout="row"
-                        fxLayoutGap="10px"
-                        fxLayoutAlign="start center"
-                        fxFlex="100"
-                        style="margin-top: 10px; align-items: center"
-                    >
-                        <div fxFlex>
-                            <mat-form-field
-                                class="w-100"
-                                color="accent"
-                                appearance="outline"
-                            >
-                                <mat-label>{{ 'Value' | translate 
}}</mat-label>
-                                <input
-                                    matInput
-                                    [(ngModel)]="mapping.value"
-                                    (ngModelChange)="updateMapping()"
-                                />
-                            </mat-form-field>
-                        </div>
-                        <div fxFlex="70px">
-                            <input
-                                [(colorPicker)]="mapping.color"
-                                [style.background]="mapping.color"
-                                style="
-                                    height: 50%;
-                                    width: 100%;
-                                    border: none;
-                                    border-radius: 10%;
-                                    cursor: pointer;
-                                "
-                                (colorPickerChange)="updateColor(i, $event)"
-                                readonly
-                            />
-                        </div>
-                        <div fxLayoutAlign="end center">
-                            <button
-                                mat-icon-button
-                                [matTooltip]="'Remove Mapping' | translate"
-                                color="accent"
-                                (click)="removeMapping(i)"
-                            >
-                                <i class="material-icons">delete</i>
-                            </button>
-                        </div>
-                    </div>
-                </div>
-            </div>
+            </sp-color-mapping-options-config>
         </div>
     </sp-configuration-box>
 </sp-visualization-config-outer>
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.ts
 
b/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.ts
index 63d7c4ebbe..7404a0c04d 100644
--- 
a/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.ts
+++ 
b/ui/src/app/data-explorer-shared/components/charts/pie/config/pie-chart-widget-config.component.ts
@@ -23,7 +23,6 @@ import {
     PieChartWidgetModel,
 } from '../model/pie-chart-widget.model';
 import { DataExplorerField } from '@streampipes/platform-services';
-import { ColorMappingService } from 
'../../../../services/color-mapping.service';
 import { ChartConfigurationService } from 
'../../../../services/chart-configuration.service';
 import { DataExplorerFieldProviderService } from 
'../../../../services/data-explorer-field-provider-service';
 
@@ -36,7 +35,6 @@ export class SpPieChartWidgetConfigComponent extends 
BaseWidgetConfig<
     PieChartVisConfig
 > {
     constructor(
-        private colorMappingService: ColorMappingService,
         widgetConfigurationService: ChartConfigurationService,
         fieldService: DataExplorerFieldProviderService,
     ) {
@@ -46,7 +44,7 @@ export class SpPieChartWidgetConfigComponent extends 
BaseWidgetConfig<
     setSelectedProperty(field: DataExplorerField) {
         this.currentlyConfiguredWidget.visualizationConfig.selectedProperty =
             field;
-        this.triggerViewRefresh();
+        this.triggerDataRefresh();
     }
 
     protected applyWidgetConfig(config: PieChartVisConfig): void {
@@ -57,8 +55,6 @@ export class SpPieChartWidgetConfigComponent extends 
BaseWidgetConfig<
         );
         config.roundingValue ??= 0.1;
         config.selectedRadius ??= 0;
-        config.showCustomColorMapping ??= false;
-        config.colorMappings ??= [];
     }
 
     updateRoundingValue(selectedType: number) {
@@ -73,53 +69,14 @@ export class SpPieChartWidgetConfigComponent extends 
BaseWidgetConfig<
         this.triggerViewRefresh();
     }
 
-    showCustomColorMapping(showCustomColorMapping: boolean) {
-        
this.currentlyConfiguredWidget.visualizationConfig.showCustomColorMapping =
-            showCustomColorMapping;
-
-        if (!showCustomColorMapping) {
-            this.resetColorMappings();
-        }
-
-        this.triggerViewRefresh();
-    }
-
-    resetColorMappings(): void {
-        this.currentlyConfiguredWidget.visualizationConfig.colorMappings = [];
-        this.triggerViewRefresh();
-    }
-
-    addMapping() {
-        this.colorMappingService.addMapping(
-            this.currentlyConfiguredWidget.visualizationConfig.colorMappings,
-        );
-        this.triggerViewRefresh();
-    }
-
-    removeMapping(index: number) {
-        this.currentlyConfiguredWidget.visualizationConfig.colorMappings =
-            this.colorMappingService.removeMapping(
-                this.currentlyConfiguredWidget.visualizationConfig
-                    .colorMappings,
-                index,
-            );
-        this.triggerViewRefresh();
-    }
-
-    updateColor(index: number, newColor: string) {
-        this.colorMappingService.updateColor(
-            this.currentlyConfiguredWidget.visualizationConfig.colorMappings,
-            index,
-            newColor,
-        );
-        this.triggerViewRefresh();
-    }
-
-    updateMapping() {
-        this.triggerViewRefresh();
-    }
-
     protected requiredFieldsForChartPresent(): boolean {
         return this.fieldProvider.allFields.length > 0;
     }
+
+    triggerViewUpdate() {
+        this.widgetConfigurationService.notify({
+            refreshView: true,
+            refreshData: false,
+        });
+    }
 }
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
 
b/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
index ad7339776e..2dfbe624db 100644
--- 
a/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
+++ 
b/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
@@ -28,7 +28,8 @@ export interface PieChartVisConfig extends 
DataExplorerVisConfig {
     roundingValue: number;
     selectedRadius: number;
     showCustomColorMapping: boolean;
-    colorMappings: { value: string; color: string }[];
+    isSelectedPropertyBoolean: boolean;
+    colorMappingsPieChart: { value: string; label: string; color: string }[];
 }
 
 export interface PieChartWidgetModel extends DataExplorerWidgetModel {
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/pie/pie-renderer.service.ts 
b/ui/src/app/data-explorer-shared/components/charts/pie/pie-renderer.service.ts
index 20a74dea05..4428968c0e 100644
--- 
a/ui/src/app/data-explorer-shared/components/charts/pie/pie-renderer.service.ts
+++ 
b/ui/src/app/data-explorer-shared/components/charts/pie/pie-renderer.service.ts
@@ -61,8 +61,27 @@ export class SpPieRendererService extends 
SpBaseSingleFieldEchartsRenderer<
         );
     }
 
-    addAdditionalConfigs(option: EChartsOption) {
-        // do nothing
+    addAdditionalConfigs(
+        option: EChartsOption,
+        widgetConfig: PieChartWidgetModel,
+    ): void {
+        if (
+            widgetConfig.visualizationConfig.selectedProperty
+                .fieldCharacteristics.binary
+        ) {
+            option.legend = { show: false };
+        } else {
+            option.legend = {
+                type: 'scroll',
+                formatter: name => {
+                    return (
+                        
widgetConfig.visualizationConfig.colorMappingsPieChart.find(
+                            c => String(c.value) === name,
+                        )?.label || name
+                    );
+                },
+            };
+        }
     }
 
     addSeriesItem(
@@ -71,7 +90,9 @@ export class SpPieRendererService extends 
SpBaseSingleFieldEchartsRenderer<
         _widgetConfig: PieChartWidgetModel,
     ): PieSeriesOption {
         const innerRadius = _widgetConfig.visualizationConfig.selectedRadius;
-        const colorMapping = _widgetConfig.visualizationConfig.colorMappings;
+        const colorMapping =
+            _widgetConfig.visualizationConfig.colorMappingsPieChart;
+
         return {
             name,
             type: 'pie',
@@ -79,12 +100,20 @@ export class SpPieRendererService extends 
SpBaseSingleFieldEchartsRenderer<
             datasetIndex: datasetIndex,
             tooltip: {
                 formatter: params => {
-                    return `${params.marker} ${params.value[0]} 
<b>${params.value[1]}</b> (${params.percent}%)`;
+                    const mappedLabel =
+                        colorMapping.find(
+                            c => c.value === params.value[0]?.toString(),
+                        )?.label || params.value[0];
+                    return `${params.marker} ${mappedLabel} 
<b>${params.value[1]}</b> (${params.percent}%)`;
                 },
             },
             label: {
                 formatter: params => {
-                    return `${params.value[0]} (${params.percent}%)`;
+                    const mappedLabel =
+                        colorMapping.find(
+                            c => c.value === params.value[0]?.toString(),
+                        )?.label || params.value[0];
+                    return `${mappedLabel} (${params.percent}%)`;
                 },
             },
             encode: { itemName: 'name', value: 'value' },
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.html
 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.html
new file mode 100644
index 0000000000..ea6cec6e5a
--- /dev/null
+++ 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.html
@@ -0,0 +1,48 @@
+<!--
+  ~ 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]="'Field' | translate">
+        <sp-select-single-property-config
+            [availableProperties]="fieldProvider.allFields"
+            [selectedProperty]="
+                currentlyConfiguredWidget.visualizationConfig.selectedProperty
+            "
+            (changeSelectedProperty)="setSelectedProperty($event)"
+        >
+        </sp-select-single-property-config>
+    </sp-configuration-box>
+
+    <sp-configuration-box [title]="'Settings' | translate">
+        <sp-color-mapping-options-config
+            [(colorMapping)]="
+                currentlyConfiguredWidget.visualizationConfig
+                    .colorMappingsStatusHeatmap
+            "
+            [selectedProperty]="
+                currentlyConfiguredWidget.visualizationConfig.selectedProperty
+            "
+            (viewRefreshEmitter)="triggerViewUpdate()"
+        >
+        </sp-color-mapping-options-config>
+    </sp-configuration-box>
+</sp-visualization-config-outer>
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.ts
 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.ts
new file mode 100644
index 0000000000..96dc0f1d31
--- /dev/null
+++ 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/config/status-heatmap-widget-config.component.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 {
+    StatusHeatmapVisConfig,
+    StatusHeatmapWidgetModel,
+} from '../model/status-heatmap-widget.model';
+import { DataExplorerFieldProviderService } from 
'../../../../services/data-explorer-field-provider-service';
+import { DataExplorerField } from '@streampipes/platform-services';
+
+@Component({
+    selector: 'sp-data-explorer-status-heatmap-widget-config',
+    templateUrl: './status-heatmap-widget-config.component.html',
+})
+export class StatusHeatmapWidgetConfigComponent extends BaseWidgetConfig<
+    StatusHeatmapWidgetModel,
+    StatusHeatmapVisConfig
+> {
+    constructor(
+        widgetConfigurationService: ChartConfigurationService,
+        fieldService: DataExplorerFieldProviderService,
+    ) {
+        super(widgetConfigurationService, fieldService);
+    }
+
+    setSelectedProperty(field: DataExplorerField) {
+        this.currentlyConfiguredWidget.visualizationConfig.selectedProperty =
+            field;
+        this.triggerDataRefresh();
+    }
+
+    protected applyWidgetConfig(config: StatusHeatmapVisConfig): void {
+        config.selectedProperty = this.fieldService.getSelectedField(
+            config.selectedProperty,
+            this.fieldProvider.allFields,
+            () => this.fieldProvider.allFields[0],
+        );
+    }
+
+    protected requiredFieldsForChartPresent(): boolean {
+        return this.fieldProvider.allFields.length > 0;
+    }
+
+    triggerViewUpdate() {
+        this.widgetConfigurationService.notify({
+            refreshView: true,
+            refreshData: true,
+        });
+    }
+}
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/model/status-heatmap-widget.model.ts
similarity index 76%
copy from 
ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
copy to 
ui/src/app/data-explorer-shared/components/charts/status-heatmap/model/status-heatmap-widget.model.ts
index ad7339776e..719633bebf 100644
--- 
a/ui/src/app/data-explorer-shared/components/charts/pie/model/pie-chart-widget.model.ts
+++ 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/model/status-heatmap-widget.model.ts
@@ -23,15 +23,18 @@ import {
 } from '@streampipes/platform-services';
 import { DataExplorerVisConfig } from 
'../../../../models/dataview-dashboard.model';
 
-export interface PieChartVisConfig extends DataExplorerVisConfig {
+export interface StatusHeatmapVisConfig extends DataExplorerVisConfig {
     selectedProperty: DataExplorerField;
-    roundingValue: number;
-    selectedRadius: number;
+    isSelectedPropertyBoolean: boolean;
     showCustomColorMapping: boolean;
-    colorMappings: { value: string; color: string }[];
+    colorMappingsStatusHeatmap: {
+        value: string;
+        label: string;
+        color: string;
+    }[];
 }
 
-export interface PieChartWidgetModel extends DataExplorerWidgetModel {
+export interface StatusHeatmapWidgetModel extends DataExplorerWidgetModel {
     dataConfig: DataExplorerDataConfig;
-    visualizationConfig: PieChartVisConfig;
+    visualizationConfig: StatusHeatmapVisConfig;
 }
diff --git 
a/ui/src/app/data-explorer-shared/components/charts/status-heatmap/status-heatmap-renderer.service.ts
 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/status-heatmap-renderer.service.ts
new file mode 100644
index 0000000000..1c35910d37
--- /dev/null
+++ 
b/ui/src/app/data-explorer-shared/components/charts/status-heatmap/status-heatmap-renderer.service.ts
@@ -0,0 +1,192 @@
+/*
+ * 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 { SpBaseEchartsRenderer } from 
'../../../echarts-renderer/base-echarts-renderer';
+import { StatusHeatmapWidgetModel } from './model/status-heatmap-widget.model';
+import { GeneratedDataset, TagValue } from '../../../models/dataset.model';
+import { EChartsOption } from 'echarts';
+import {
+    DimensionDefinitionLoose,
+    OptionDataValue,
+    OptionSourceDataArrayRows,
+} from 'echarts/types/src/util/types';
+import { Injectable } from '@angular/core';
+import { FieldUpdateInfo } from '../../../models/field-update.model';
+import { ColorMappingService } from '../../../services/color-mapping.service';
+
+@Injectable({ providedIn: 'root' })
+export class SpStatusHeatmapRendererService extends 
SpBaseEchartsRenderer<StatusHeatmapWidgetModel> {
+    constructor(private colorMappingService: ColorMappingService) {
+        super();
+    }
+    applyOptions(
+        generatedDataset: GeneratedDataset,
+        options: EChartsOption,
+        widgetConfig: StatusHeatmapWidgetModel,
+    ): void {
+        this.basicOptions(options);
+
+        const field = widgetConfig.visualizationConfig.selectedProperty;
+        const sourceIndex = field.sourceIndex;
+
+        const rawDataset = this.datasetUtilsService.findPreparedDataset(
+            generatedDataset,
+            sourceIndex,
+        );
+        const rawDatasetSource: OptionSourceDataArrayRows = rawDataset
+            .rawDataset.source as OptionSourceDataArrayRows;
+        const tags = rawDataset.tagValues;
+        const statusIndex = rawDataset.rawDataset.dimensions.indexOf(
+            field.fullDbName,
+        );
+
+        const colorMapping =
+            widgetConfig.visualizationConfig.colorMappingsStatusHeatmap;
+
+        rawDatasetSource.shift();
+        rawDatasetSource.sort((a, b) => {
+            return new Date(a[0]).getTime() - new Date(b[0]).getTime();
+        });
+
+        const uniqueValues = [
+            ...new Set(rawDatasetSource.map(row => row[statusIndex])),
+        ];
+        const valueMapping = new Map(
+            uniqueValues.map((val, index) => [val, index]),
+        );
+
+        const transformedDataset = rawDatasetSource.map((row, index) => {
+            let statusValue = row[statusIndex];
+
+            if (typeof statusValue === 'boolean') {
+                statusValue = statusValue ? 1 : 0;
+            } else if (
+                typeof statusValue === 'string' ||
+                typeof statusValue === 'number'
+            ) {
+                statusValue = valueMapping.get(statusValue) ?? null;
+            }
+
+            return [
+                index,
+                this.makeTag(rawDataset.rawDataset.dimensions, tags, row),
+                statusValue,
+            ];
+        });
+
+        options.dataset = { source: transformedDataset };
+
+        (options.xAxis as any).data = rawDatasetSource.map(s => {
+            return new Date(s[0]).toLocaleString();
+        });
+
+        options.tooltip = {
+            formatter: params => {
+                const timestamp = rawDatasetSource[params.data[0]][0];
+                const statusValue = params.value[2];
+                const originalValue = uniqueValues[statusValue];
+
+                const statusLabel =
+                    colorMapping.find(c => c.value === 
originalValue.toString())
+                        ?.label || originalValue;
+
+                return `${params.marker} ${new 
Date(timestamp).toLocaleString()}<br/>Status: <b>${statusLabel}</b>`;
+            },
+        };
+
+        const dynamicPieces = uniqueValues.map((val, index) => ({
+            value: index,
+            label:
+                colorMapping.find(c => c.value === val.toString())?.label ||
+                val.toString(),
+            color:
+                colorMapping.find(c => c.value === val.toString())?.color ||
+                this.colorMappingService.getDefaultColor(index),
+        }));
+
+        options.visualMap = {
+            type: 'piecewise',
+            pieces: dynamicPieces,
+            orient: 'horizontal',
+            right: '5%',
+            top: '20',
+        };
+
+        options.legend = {
+            type: 'scroll',
+        };
+
+        options.series = [
+            {
+                name: '',
+                type: 'heatmap',
+                datasetIndex: 0,
+                encode: {
+                    itemId: 0,
+                    value: 2,
+                },
+                emphasis: {
+                    itemStyle: {
+                        shadowBlur: 10,
+                        shadowColor: 'rgba(0, 0, 0, 0.5)',
+                    },
+                },
+            },
+        ];
+    }
+
+    public handleUpdatedFields(
+        fieldUpdateInfo: FieldUpdateInfo,
+        widgetConfig: StatusHeatmapWidgetModel,
+    ): void {
+        this.fieldUpdateService.updateAnyField(
+            widgetConfig.visualizationConfig.selectedProperty,
+            fieldUpdateInfo,
+        );
+    }
+
+    basicOptions(options: EChartsOption): void {
+        options.grid = {
+            height: '80%',
+            top: '80',
+        };
+        options.xAxis = {
+            type: 'category',
+            splitArea: { show: true },
+        };
+        options.yAxis = {
+            type: 'category',
+            splitArea: { show: true },
+        };
+    }
+
+    private makeTag(
+        dimensions: DimensionDefinitionLoose[],
+        tags: TagValue[],
+        row: Array<OptionDataValue>,
+    ) {
+        if (tags.length > 0) {
+            return tags[0].tagKeys
+                .map(key => {
+                    const index = dimensions.indexOf(key);
+                    return row[index];
+                })
+                .toString();
+        }
+    }
+}
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 fa32f6acd5..562b5e5aa7 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
@@ -80,6 +80,7 @@ import { StatusWidgetConfigComponent } from 
'./components/charts/status/config/s
 import { MapWidgetConfigComponent } from 
'./components/charts/map/config/map-widget-config.component';
 import { MapWidgetComponent } from 
'./components/charts/map/map-widget.component';
 import { HeatmapWidgetConfigComponent } from 
'./components/charts/heatmap/config/heatmap-widget-config.component';
+import { StatusHeatmapWidgetConfigComponent } from 
'./components/charts/status-heatmap/config/status-heatmap-widget-config.component';
 import { ImageViewerComponent } from 
'./components/charts/image/image-viewer/image-viewer.component';
 import { ChartDirective } from './components/chart-container/chart.directive';
 import { TooMuchDataComponent } from 
'./components/charts/base/too-much-data/too-much-data.component';
@@ -96,6 +97,7 @@ import { SpEchartsWidgetAppearanceConfigComponent } from 
'./components/chart-con
 import { SpTimeSeriesAppearanceConfigComponent } from 
'./components/charts/time-series-chart/appearance-config/time-series-appearance-config.component';
 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';
 
 @NgModule({
     imports: [
@@ -164,6 +166,7 @@ import { TranslateModule } from '@ngx-translate/core';
         TrafficLightWidgetConfigComponent,
         StatusWidgetComponent,
         StatusWidgetConfigComponent,
+        StatusHeatmapWidgetConfigComponent,
         MapWidgetConfigComponent,
         MapWidgetComponent,
         HeatmapWidgetConfigComponent,
@@ -182,6 +185,7 @@ import { TranslateModule } from '@ngx-translate/core';
         SpEchartsWidgetAppearanceConfigComponent,
         SpTimeSeriesAppearanceConfigComponent,
         SpDataZoomConfigComponent,
+        ColorMappingOptionsConfigComponent,
     ],
     exports: [DataExplorerChartContainerComponent],
 })
diff --git 
a/ui/src/app/data-explorer-shared/echarts-renderer/base-single-field-echarts-renderer.ts
 
b/ui/src/app/data-explorer-shared/echarts-renderer/base-single-field-echarts-renderer.ts
index 2ceae6ad6b..f1f366b34e 100644
--- 
a/ui/src/app/data-explorer-shared/echarts-renderer/base-single-field-echarts-renderer.ts
+++ 
b/ui/src/app/data-explorer-shared/echarts-renderer/base-single-field-echarts-renderer.ts
@@ -73,7 +73,13 @@ export abstract class SpBaseSingleFieldEchartsRenderer<
             gridOptions,
         );
         this.configureAxes(options, widgetConfig, numberOfCharts, series);
-        this.finalizeOptions(options, datasets, series, gridOptions);
+        this.finalizeOptions(
+            options,
+            datasets,
+            series,
+            gridOptions,
+            widgetConfig,
+        );
     }
 
     private getNumberOfCharts(tags: TagValue[]): number {
@@ -163,6 +169,7 @@ export abstract class SpBaseSingleFieldEchartsRenderer<
         dataset: DatasetOption[],
         series: S[],
         gridOptions: GridOptions,
+        widgetConfig: T,
     ) {
         if (series.length > 1) {
             this.echartsUtilsService.addSeriesTitles(
@@ -173,7 +180,7 @@ export abstract class SpBaseSingleFieldEchartsRenderer<
         }
         options.dataset = dataset;
         options.series = series;
-        this.addAdditionalConfigs(options);
+        this.addAdditionalConfigs(options, widgetConfig);
     }
 
     showAxes(): boolean {
@@ -182,7 +189,10 @@ export abstract class SpBaseSingleFieldEchartsRenderer<
 
     abstract addDatasetTransform(widgetConfig: T): DataTransformOption;
 
-    abstract addAdditionalConfigs(option: EChartsOption): void;
+    abstract addAdditionalConfigs(
+        option: EChartsOption,
+        widgetConfig?: T,
+    ): void;
 
     abstract addSeriesItem(
         name: string,
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 9a25dc1e19..45ff00f2cb 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
@@ -23,6 +23,7 @@ import { TableWidgetComponent } from 
'../components/charts/table/table-widget.co
 import { MapWidgetConfigComponent } from 
'../components/charts/map/config/map-widget-config.component';
 import { MapWidgetComponent } from 
'../components/charts/map/map-widget.component';
 import { HeatmapWidgetConfigComponent } from 
'../components/charts/heatmap/config/heatmap-widget-config.component';
+import { StatusHeatmapWidgetConfigComponent } from 
'../components/charts/status-heatmap/config/status-heatmap-widget-config.component';
 import { TimeSeriesChartWidgetConfigComponent } from 
'../components/charts/time-series-chart/config/time-series-chart-widget-config.component';
 import { ImageWidgetConfigComponent } from 
'../components/charts/image/config/image-widget-config.component';
 import { ImageWidgetComponent } from 
'../components/charts/image/image-widget.component';
@@ -30,6 +31,7 @@ import { IndicatorWidgetConfigComponent } from 
'../components/charts/indicator/c
 import { CorrelationWidgetConfigComponent } from 
'../components/charts/correlation-chart/config/correlation-chart-widget-config.component';
 import { SpEchartsWidgetComponent } from 
'../components/charts/base/echarts-widget.component';
 import { HeatmapWidgetModel } from 
'../components/charts/heatmap/model/heatmap-widget.model';
+import { StatusHeatmapWidgetModel } from 
'../components/charts/status-heatmap/model/status-heatmap-widget.model';
 import { SpValueHeatmapWidgetConfigComponent } from 
'../components/charts/value-heatmap/config/value-heatmap-chart-widget-config.component';
 import { SpHistogramChartWidgetConfigComponent } from 
'../components/charts/histogram/config/histogram-chart-widget-config.component';
 import { SpPieChartWidgetConfigComponent } from 
'../components/charts/pie/config/pie-chart-widget-config.component';
@@ -38,6 +40,7 @@ import { PieChartWidgetModel } from 
'../components/charts/pie/model/pie-chart-wi
 import { ValueHeatmapChartWidgetModel } from 
'../components/charts/value-heatmap/model/value-heatmap-chart-widget.model';
 import { SpHistogramRendererService } from 
'../components/charts/histogram/histogram-renderer.service';
 import { SpHeatmapRendererService } from 
'../components/charts/heatmap/heatmap-renderer.service';
+import { SpStatusHeatmapRendererService } from 
'../components/charts/status-heatmap/status-heatmap-renderer.service';
 import { SpPieRendererService } from 
'../components/charts/pie/pie-renderer.service';
 import { SpValueHeatmapRendererService } from 
'../components/charts/value-heatmap/value-heatmap-renderer.service';
 import { CorrelationChartWidgetModel } from 
'../components/charts/correlation-chart/model/correlation-chart-widget.model';
@@ -65,6 +68,7 @@ export class DataExplorerChartRegistry {
     constructor(
         private gaugeRenderer: SpGaugeRendererService,
         private heatmapRenderer: SpHeatmapRendererService,
+        private statusHeatmapRenderer: SpStatusHeatmapRendererService,
         private histogramRenderer: SpHistogramRendererService,
         private pieRenderer: SpPieRendererService,
         private valueHeatmapRenderer: SpValueHeatmapRendererService,
@@ -117,6 +121,17 @@ export class DataExplorerChartRegistry {
                 widgetComponent: SpEchartsWidgetComponent<HeatmapWidgetModel>,
                 chartRenderer: this.heatmapRenderer,
             },
+            {
+                id: 'status-heatmap',
+                label: 'Status Heatmap',
+                widgetAppearanceConfigurationComponent:
+                    SpEchartsWidgetAppearanceConfigComponent,
+                widgetConfigurationComponent:
+                    StatusHeatmapWidgetConfigComponent,
+                widgetComponent:
+                    SpEchartsWidgetComponent<StatusHeatmapWidgetModel>,
+                chartRenderer: this.statusHeatmapRenderer,
+            },
             {
                 id: 'time-series-chart',
                 label: this.translateService.instant('Time Series Chart'),
diff --git a/ui/src/app/data-explorer-shared/services/color-mapping.service.ts 
b/ui/src/app/data-explorer-shared/services/color-mapping.service.ts
index 9b878a32d4..73cc9d6dcc 100644
--- a/ui/src/app/data-explorer-shared/services/color-mapping.service.ts
+++ b/ui/src/app/data-explorer-shared/services/color-mapping.service.ts
@@ -35,22 +35,25 @@ export class ColorMappingService {
     ];
     constructor() {}
 
-    addMapping(colorMappings: { value: string; color: string }[]): void {
+    addMapping(
+        colorMappings: { value: string; label: string; color: string }[],
+    ): void {
         colorMappings.push({
             value: '',
+            label: '',
             color: this.getDefaultColor(colorMappings.length),
         });
     }
 
     removeMapping(
-        colorMappings: { value: string; color: string }[],
+        colorMappings: { value: string; label: string; color: string }[],
         index: number,
-    ): { value: string; color: string }[] {
+    ): { value: string; label: string; color: string }[] {
         return colorMappings.filter((_, i) => i !== index);
     }
 
     updateColor(
-        currentMappings: { value: string; color: string }[],
+        currentMappings: { value: string; label: string; color: string }[],
         index: number,
         newColor: string,
     ): void {

Reply via email to