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

dominikriemer pushed a commit to branch 
improve-grouping-mode-of-time-series-chart
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 87d08b302118787049c671d64be82db86dc29bd2
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Jun 11 23:03:22 2026 +0200

    feat: Add coloring modes to time series chart in grouping mode
---
 ui/deployment/i18n/de.json                         |   3 +
 ui/deployment/i18n/en.json                         |   3 +
 ui/deployment/i18n/pl.json                         |   3 +
 .../color-mapping-options-config.component.html    |  30 +++---
 .../color-mapping-options-config.component.ts      |   6 +-
 .../time-series-item-config.component.html         |  66 +++++++++----
 .../time-series-item-config.component.ts           |  98 ++++++++++++++++---
 .../time-series-chart-widget-config.component.ts   |  30 +++++-
 .../model/time-series-chart-widget.model.ts        |  10 ++
 .../sp-timeseries-renderer.service.ts              |  24 +++--
 .../services/chart-colorization.service.ts         | 106 ++++++++++++++-------
 11 files changed, 290 insertions(+), 89 deletions(-)

diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 922fcc8568..1afccd42a4 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -67,6 +67,7 @@
   "Add and edit user accounts": "Benutzerkonten hinzufügen und bearbeiten",
   "Add condition": "Bedingung hinzufügen",
   "Add custom color mapping": "Benutzerdefiniertes Mapping hinzufügen",
+  "Custom color mapping": "Benutzerdefiniertes Farb-Mapping",
   "Add exact location": "Genauen Standort hinzufügen",
   "Add field": "Feld hinzufügen",
   "Add from template": "Aus Vorlage hinzufügen",
@@ -489,6 +490,7 @@
   "Group ID": "Gruppen-ID",
   "Group Name": "Name der Benutzergruppe",
   "Group by": "Gruppieren nach",
+  "Grouped colors": "Farben für Gruppierung",
   "Group name": "Name der Benutzergruppe",
   "Group selection": "Auswahl der Gruppe",
   "Grouped": "Gruppiert",
@@ -1000,6 +1002,7 @@
   "Start pipeline": "Pipeline starten",
   "Start pipeline immediately": "Pipeline sofort starten",
   "Start selected adapters": "Ausgewählte Adapter starten",
+  "Stable palette": "Stabile Palette",
   "Starting": "Starte",
   "Starting adapter": "Adapter wird gestartet",
   "Starting adapter {{adapterName}}": "Adapter starten {{adapterName}}",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index af22414388..5f0b743c7a 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -67,6 +67,7 @@
   "Add and edit user accounts": null,
   "Add condition": null,
   "Add custom color mapping": null,
+  "Custom color mapping": null,
   "Add exact location": null,
   "Add field": null,
   "Add from template": null,
@@ -489,6 +490,7 @@
   "Group ID": null,
   "Group Name": null,
   "Group by": null,
+  "Grouped colors": null,
   "Group name": null,
   "Group selection": null,
   "Grouped": null,
@@ -1000,6 +1002,7 @@
   "Start pipeline": null,
   "Start pipeline immediately": null,
   "Start selected adapters": null,
+  "Stable palette": null,
   "Starting": null,
   "Starting adapter": null,
   "Starting adapter {{adapterName}}": "Starting adapter {{adapterName}}",
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 1f9fcbcd68..84f58a1a7c 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -67,6 +67,7 @@
   "Add and edit user accounts": "Dodawaj i edytuj konta użytkowników",
   "Add condition": "Dodaj warunek",
   "Add custom color mapping": "Dodaj własne mapowanie kolorów",
+  "Custom color mapping": "Własne mapowanie kolorów",
   "Add exact location": "Dodaj dokładną lokalizację",
   "Add field": "Dodaj pole",
   "Add from template": "Dodaj z szablonu",
@@ -489,6 +490,7 @@
   "Group ID": "ID grupy",
   "Group Name": "Nazwa grupy",
   "Group by": "Grupuj według",
+  "Grouped colors": "Kolory grupowania",
   "Group name": "Nazwa grupy użytkowników",
   "Group selection": "Wybór grupy",
   "Grouped": "Zgrupowane",
@@ -1000,6 +1002,7 @@
   "Start pipeline": "Uruchom strumień",
   "Start pipeline immediately": "Uruchom strumień od razu",
   "Start selected adapters": "Uruchom wybrane adaptery",
+  "Stable palette": "Stabilna paleta",
   "Starting": "Uruchamianie",
   "Starting adapter": "Uruchamianie adaptera",
   "Starting adapter {{adapterName}}": "Uruchamianie adaptera {{adapterName}}",
diff --git 
a/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
 
b/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
index df661e5716..b4767037d6 100644
--- 
a/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
+++ 
b/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.html
@@ -16,21 +16,23 @@
 ~
 -->
 <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)"
+    @if (showToggle) {
+        <div
+            fxLayout="row"
+            fxLayoutGap="10px"
+            fxLayoutAlign="start center"
+            fxFlex="1"
+            class="checkbox-container"
         >
-        </mat-checkbox>
-        <small>{{ 'Add custom color mapping' | translate }}</small>
-    </div>
+            <mat-checkbox
+                color="accent"
+                [(ngModel)]="showCustomColorMapping"
+                (ngModelChange)="setCustomColorMapping($event)"
+            >
+            </mat-checkbox>
+            <small>{{ 'Add custom color mapping' | translate }}</small>
+        </div>
+    }
 
     @if (!isSelectedPropertyBoolean && showCustomColorMapping) {
         <div>
diff --git 
a/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
 
b/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
index 03380eddb0..2cbdaf51c0 100644
--- 
a/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
+++ 
b/ui/src/app/chart-shared/components/chart-config/color-mapping-options-config/color-mapping-options-config.component.ts
@@ -72,10 +72,12 @@ export class ColorMappingOptionsConfigComponent implements 
OnInit, OnChanges {
 
     @Input() colorMapping: { value: string; label: string; color: string }[];
 
-    @Input() selectedProperty: DataExplorerField;
+    @Input() selectedProperty?: DataExplorerField;
 
     @Input() showCustomColorMapping: boolean;
 
+    @Input() showToggle = true;
+
     @Output()
     viewRefreshEmitter: EventEmitter<void> = new EventEmitter<void>();
 
@@ -170,7 +172,7 @@ export class ColorMappingOptionsConfigComponent implements 
OnInit, OnChanges {
     }
 
     isBooleanPropertySelected(): boolean {
-        return this.selectedProperty.fieldCharacteristics.binary;
+        return this.selectedProperty?.fieldCharacteristics.binary ?? false;
     }
 
     setCustomColorMapping(showCustomColorMapping: boolean) {
diff --git 
a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
 
b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
index ac1ba8e095..1a64d5092d 100644
--- 
a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
+++ 
b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.html
@@ -38,7 +38,7 @@
             [ngStyle]="{
                 background:
                     currentlyConfiguredWidget.visualizationConfig.chosenColor[
-                        field.fullDbName + field.sourceIndex.toString()
+                        getFieldKey(field)
                     ],
             }"
         ></span>
@@ -66,15 +66,11 @@
                         class="color-field"
                         [(colorPicker)]="
                             currentlyConfiguredWidget.visualizationConfig
-                                .chosenColor[
-                                field.fullDbName + field.sourceIndex.toString()
-                            ]
+                                .chosenColor[getFieldKey(field)]
                         "
                         [style.background]="
                             currentlyConfiguredWidget.visualizationConfig
-                                .chosenColor[
-                                field.fullDbName + field.sourceIndex.toString()
-                            ]
+                                .chosenColor[getFieldKey(field)]
                         "
                         required
                         [cpPosition]="'bottom'"
@@ -97,10 +93,7 @@
                         <input
                             [(ngModel)]="
                                 currentlyConfiguredWidget.visualizationConfig
-                                    .displayName[
-                                    field.fullDbName +
-                                        field.sourceIndex.toString()
-                                ]
+                                    .displayName[getFieldKey(field)]
                             "
                             matInput
                             (input)="
@@ -124,10 +117,7 @@
                         <mat-select
                             [(value)]="
                                 currentlyConfiguredWidget.visualizationConfig
-                                    .displayType[
-                                    field.fullDbName +
-                                        field.sourceIndex.toString()
-                                ]
+                                    .displayType[getFieldKey(field)]
                             "
                             (selectionChange)="viewRefreshEmitter.emit()"
                         >
@@ -163,10 +153,7 @@
                         <mat-select
                             [(value)]="
                                 currentlyConfiguredWidget.visualizationConfig
-                                    .chosenAxis[
-                                    field.fullDbName +
-                                        field.sourceIndex.toString()
-                                ]
+                                    .chosenAxis[getFieldKey(field)]
                             "
                             (selectionChange)="viewRefreshEmitter.emit()"
                         >
@@ -180,6 +167,47 @@
                     </mat-form-field>
                 </div>
             </div>
+            @if (hasGrouping(field)) {
+                <div fxLayout="row" fxFlex="100">
+                    <div fxFlex="30" fxLayoutAlign="start center">
+                        <small>{{ 'Grouped colors' | translate }}</small>
+                    </div>
+                    <div fxFlex="70" fxLayoutAlign="start center">
+                        <mat-form-field
+                            appearance="outline"
+                            color="accent"
+                            class="w-100"
+                        >
+                            <mat-select
+                                [value]="getGroupedColorMode(field)"
+                                (selectionChange)="
+                                    setGroupedColorMode(field, $event.value)
+                                "
+                            >
+                                <mat-option [value]="'stable_palette'">{{
+                                    'Stable palette' | translate
+                                }}</mat-option>
+                                <mat-option [value]="'custom_mapping'">{{
+                                    'Custom color mapping' | translate
+                                }}</mat-option>
+                            </mat-select>
+                        </mat-form-field>
+                    </div>
+                </div>
+                @if (getGroupedColorMode(field) === 'custom_mapping') {
+                    <sp-color-mapping-options-config
+                        class="mt-sm"
+                        [colorMapping]="getGroupedColorMappings(field)"
+                        [showCustomColorMapping]="true"
+                        [showToggle]="false"
+                        (colorMappingChange)="
+                            setGroupedColorMappings(field, $event)
+                        "
+                        (viewRefreshEmitter)="viewRefreshEmitter.emit()"
+                    >
+                    </sp-color-mapping-options-config>
+                }
+            }
         </div>
     }
 </div>
diff --git 
a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
 
b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
index f15ce41305..fc23ef2c40 100644
--- 
a/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
+++ 
b/ui/src/app/chart-shared/components/chart-config/select-color-properties-config/time-series-item-config/time-series-item-config.component.ts
@@ -18,16 +18,19 @@
 
 import { Component, EventEmitter, Input, Output } from '@angular/core';
 import { DataExplorerField } from '@streampipes/platform-services';
-import { TimeSeriesChartWidgetModel } from 
'../../../charts/time-series-chart/model/time-series-chart-widget.model';
 import {
-    DefaultFlexDirective,
-    DefaultLayoutAlignDirective,
-    DefaultLayoutDirective,
-    DefaultLayoutGapDirective,
+    TimeSeriesChartWidgetModel,
+    TimeSeriesGroupColorMapping,
+    TimeSeriesGroupedColorMode,
+} from 
'../../../charts/time-series-chart/model/time-series-chart-widget.model';
+import {
+    FlexDirective,
+    LayoutAlignDirective,
+    LayoutGapDirective,
 } from '@ngbracket/ngx-layout/flex';
 import { MatCheckbox } from '@angular/material/checkbox';
 import { NgStyle } from '@angular/common';
-import { DefaultStyleDirective } from '@ngbracket/ngx-layout/extended';
+import { StyleDirective } from '@ngbracket/ngx-layout/extended';
 import { MatIconButton } from '@angular/material/button';
 import { MatTooltip } from '@angular/material/tooltip';
 import { ColorPickerDirective } from 'ngx-color-picker';
@@ -36,21 +39,23 @@ import { MatInput } from '@angular/material/input';
 import { FormsModule } from '@angular/forms';
 import { MatOption, MatSelect } from '@angular/material/select';
 import { TranslatePipe } from '@ngx-translate/core';
+import { ColorMappingOptionsConfigComponent } from 
'../../color-mapping-options-config/color-mapping-options-config.component';
+import { LayoutDirective } from '@ngbracket/ngx-layout';
 
 @Component({
     selector: 'sp-time-series-item-config',
     templateUrl: './time-series-item-config.component.html',
     styleUrls: ['./time-series-item-config.component.scss'],
     imports: [
-        DefaultLayoutDirective,
-        DefaultLayoutAlignDirective,
+        LayoutGapDirective,
+        LayoutAlignDirective,
         MatCheckbox,
-        DefaultFlexDirective,
+        FlexDirective,
         NgStyle,
-        DefaultStyleDirective,
+        StyleDirective,
         MatIconButton,
         MatTooltip,
-        DefaultLayoutGapDirective,
+        LayoutGapDirective,
         ColorPickerDirective,
         MatFormField,
         MatInput,
@@ -58,6 +63,8 @@ import { TranslatePipe } from '@ngx-translate/core';
         MatSelect,
         MatOption,
         TranslatePipe,
+        ColorMappingOptionsConfigComponent,
+        LayoutDirective,
     ],
 })
 export class SpTimeseriesItemConfigComponent {
@@ -122,4 +129,73 @@ export class SpTimeseriesItemConfigComponent {
         ] = searchValue;
         this.viewRefreshEmitter.emit();
     }
+
+    getFieldKey(field: DataExplorerField): string {
+        return field.fullDbName + field.sourceIndex.toString();
+    }
+
+    hasGrouping(field: DataExplorerField): boolean {
+        return (
+            this.currentlyConfiguredWidget.dataConfig.sourceConfigs[
+                field.sourceIndex
+            ]?.queryConfig.groupBy?.some(
+                groupByField => groupByField.selected,
+            ) ?? false
+        );
+    }
+
+    getGroupedColorMode(field: DataExplorerField): TimeSeriesGroupedColorMode {
+        this.currentlyConfiguredWidget.visualizationConfig.groupedColorMode ??=
+            {};
+        return (
+            
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMode[
+                this.getFieldKey(field)
+            ] ?? 'stable_palette'
+        );
+    }
+
+    setGroupedColorMode(
+        field: DataExplorerField,
+        mode: TimeSeriesGroupedColorMode,
+    ): void {
+        this.currentlyConfiguredWidget.visualizationConfig.groupedColorMode ??=
+            {};
+        this.currentlyConfiguredWidget.visualizationConfig.groupedColorMode[
+            this.getFieldKey(field)
+        ] = mode;
+        this.viewRefreshEmitter.emit();
+    }
+
+    getGroupedColorMappings(
+        field: DataExplorerField,
+    ): TimeSeriesGroupColorMapping[] {
+        const fieldKey = this.getFieldKey(field);
+        
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMappings ??=
+            {};
+        const mappings =
+            this.currentlyConfiguredWidget.visualizationConfig
+                .groupedColorMappings[fieldKey];
+
+        if (!mappings) {
+            
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMappings[
+                fieldKey
+            ] = [];
+            return this.currentlyConfiguredWidget.visualizationConfig
+                .groupedColorMappings[fieldKey];
+        }
+
+        return mappings;
+    }
+
+    setGroupedColorMappings(
+        field: DataExplorerField,
+        mappings: TimeSeriesGroupColorMapping[],
+    ): void {
+        
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMappings ??=
+            {};
+        
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMappings[
+            this.getFieldKey(field)
+        ] = mappings;
+        this.viewRefreshEmitter.emit();
+    }
 }
diff --git 
a/ui/src/app/chart-shared/components/charts/time-series-chart/config/time-series-chart-widget-config.component.ts
 
b/ui/src/app/chart-shared/components/charts/time-series-chart/config/time-series-chart-widget-config.component.ts
index 1b33620f18..2c8bb69567 100644
--- 
a/ui/src/app/chart-shared/components/charts/time-series-chart/config/time-series-chart-widget-config.component.ts
+++ 
b/ui/src/app/chart-shared/components/charts/time-series-chart/config/time-series-chart-widget-config.component.ts
@@ -84,6 +84,12 @@ export class TimeSeriesChartWidgetConfigComponent extends 
BaseWidgetConfig<
             this.currentlyConfiguredWidget.visualizationConfig.displayType;
         const currentAxis =
             this.currentlyConfiguredWidget.visualizationConfig.chosenAxis;
+        const currentGroupedColorMode =
+            this.currentlyConfiguredWidget.visualizationConfig
+                .groupedColorMode ?? {};
+        const currentGroupedColorMappings =
+            this.currentlyConfiguredWidget.visualizationConfig
+                .groupedColorMappings ?? {};
 
         const lenBefore = Object.keys(currentAxis).length;
 
@@ -94,6 +100,8 @@ export class TimeSeriesChartWidgetConfigComponent extends 
BaseWidgetConfig<
                 currentNames[name] = field.fullDbName;
                 currentTypes[name] = 'lines';
                 currentAxis[name] = 'left';
+                currentGroupedColorMode[name] = 'stable_palette';
+                currentGroupedColorMappings[name] = [];
             }
         });
 
@@ -105,6 +113,10 @@ export class TimeSeriesChartWidgetConfigComponent extends 
BaseWidgetConfig<
             currentTypes;
         this.currentlyConfiguredWidget.visualizationConfig.chosenAxis =
             currentAxis;
+        this.currentlyConfiguredWidget.visualizationConfig.groupedColorMode =
+            currentGroupedColorMode;
+        
this.currentlyConfiguredWidget.visualizationConfig.groupedColorMappings =
+            currentGroupedColorMappings;
 
         this.triggerViewRefresh();
     }
@@ -140,6 +152,16 @@ export class TimeSeriesChartWidgetConfigComponent extends 
BaseWidgetConfig<
             this.numericPlusBooleanFields,
             () => 'left',
         );
+        config.groupedColorMode = this.getConfigOrDefault(
+            config.groupedColorMode,
+            this.numericPlusBooleanFields,
+            () => 'stable_palette',
+        );
+        config.groupedColorMappings = this.getConfigOrDefault(
+            config.groupedColorMappings,
+            this.numericPlusBooleanFields,
+            () => [],
+        );
 
         config.yKeys = [];
         config.selectedTimeSeriesChartProperties =
@@ -165,11 +187,11 @@ export class TimeSeriesChartWidgetConfigComponent extends 
BaseWidgetConfig<
         };
     }
 
-    private getConfigOrDefault(
-        config: Record<string, any>,
+    private getConfigOrDefault<T>(
+        config: Record<string, T>,
         availableFields: DataExplorerField[],
-        getDefaultValue: (field: DataExplorerField, index: number) => string,
-    ) {
+        getDefaultValue: (field: DataExplorerField, index: number) => T,
+    ): Record<string, T> {
         const fieldKeys = availableFields.map(
             f => f.fullDbName + f.sourceIndex,
         );
diff --git 
a/ui/src/app/chart-shared/components/charts/time-series-chart/model/time-series-chart-widget.model.ts
 
b/ui/src/app/chart-shared/components/charts/time-series-chart/model/time-series-chart-widget.model.ts
index 4cc95bc86f..24a3a89bb4 100644
--- 
a/ui/src/app/chart-shared/components/charts/time-series-chart/model/time-series-chart-widget.model.ts
+++ 
b/ui/src/app/chart-shared/components/charts/time-series-chart/model/time-series-chart-widget.model.ts
@@ -27,6 +27,14 @@ import {
     DataExplorerVisConfig,
 } from '../../../../models/dataview-dashboard.model';
 
+export type TimeSeriesGroupedColorMode = 'stable_palette' | 'custom_mapping';
+
+export interface TimeSeriesGroupColorMapping {
+    value: string;
+    label: string;
+    color: string;
+}
+
 export interface TimeSeriesChartVisConfig extends DataExplorerVisConfig {
     selectedTimeSeriesChartProperties: DataExplorerField[];
     selectedBackgroundColorProperty?: EventPropertyUnion;
@@ -39,6 +47,8 @@ export interface TimeSeriesChartVisConfig extends 
DataExplorerVisConfig {
     displayName: { [id: string]: string };
     displayType: { [id: string]: string };
     chosenAxis: { [id: string]: string };
+    groupedColorMode: { [id: string]: TimeSeriesGroupedColorMode };
+    groupedColorMappings: { [id: string]: TimeSeriesGroupColorMapping[] };
     leftAxis: AxisConfig;
     rightAxis: AxisConfig;
     showSpike: boolean;
diff --git 
a/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
 
b/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
index cdc652878a..ebd417bfa5 100644
--- 
a/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
+++ 
b/ui/src/app/chart-shared/components/charts/time-series-chart/sp-timeseries-renderer.service.ts
@@ -27,7 +27,11 @@ import { Injectable } from '@angular/core';
 import { TimeSeriesChartWidgetModel } from 
'./model/time-series-chart-widget.model';
 import { DataExplorerField } from '@streampipes/platform-services';
 import { SpBaseEchartsRenderer } from 
'../../../echarts-renderer/base-echarts-renderer';
-import { GeneratedDataset, WidgetSize } from '../../../models/dataset.model';
+import {
+    GeneratedDataset,
+    TagValue,
+    WidgetSize,
+} from '../../../models/dataset.model';
 import {
     AxisConfig,
     WidgetBaseAppearanceConfig,
@@ -71,12 +75,18 @@ export class SpTimeseriesRendererService extends 
SpBaseEchartsRenderer<TimeSerie
                         widgetConfig.visualizationConfig.displayName[
                             field.fullDbName + field.sourceIndex
                         ];
+                    const mappedGroupLabel = 
this.colorizationService.findLabel(
+                        widgetConfig.visualizationConfig,
+                        field,
+                        tag,
+                    );
                     const seriesName =
                         dataset.groupedDatasets.length > 0
-                            ? this.echartsUtilsService.toTagString(
+                            ? (mappedGroupLabel ??
+                              this.echartsUtilsService.toTagString(
                                   tag,
                                   displayName,
-                              )
+                              ))
                             : displayName;
                     const fieldIndex = rawDatasetDimensions.indexOf(
                         field.fullDbName,
@@ -85,10 +95,10 @@ export class SpTimeseriesRendererService extends 
SpBaseEchartsRenderer<TimeSerie
                         this.makeSeries(
                             widgetConfig,
                             i,
-                            groupIndex,
                             field,
                             fieldIndex,
                             seriesName,
+                            tag,
                         ),
                     );
                 }
@@ -136,19 +146,19 @@ export class SpTimeseriesRendererService extends 
SpBaseEchartsRenderer<TimeSerie
     makeSeries(
         widgetConfig: TimeSeriesChartWidgetModel,
         datasetIndex: number,
-        groupIndex: number,
         field: DataExplorerField,
         fieldIndex: number,
         seriesName: string,
+        tag?: TagValue,
     ): SeriesOption {
         const seriesType = this.makeSeriesType(
             widgetConfig.visualizationConfig.displayType,
             field,
         );
         const color = this.colorizationService.makeColor(
-            widgetConfig.visualizationConfig.chosenColor,
+            widgetConfig.visualizationConfig,
             field,
-            groupIndex,
+            tag,
         );
 
         const series = {
diff --git a/ui/src/app/chart-shared/services/chart-colorization.service.ts 
b/ui/src/app/chart-shared/services/chart-colorization.service.ts
index 35817bc944..eb2a3057b1 100644
--- a/ui/src/app/chart-shared/services/chart-colorization.service.ts
+++ b/ui/src/app/chart-shared/services/chart-colorization.service.ts
@@ -18,53 +18,95 @@
 
 import { Injectable } from '@angular/core';
 import { DataExplorerField } from '@streampipes/platform-services';
+import {
+    TimeSeriesChartVisConfig,
+    TimeSeriesGroupColorMapping,
+} from 
'../components/charts/time-series-chart/model/time-series-chart-widget.model';
+import { TagValue } from '../models/dataset.model';
+import { ColorMappingService } from './color-mapping.service';
 
 @Injectable({ providedIn: 'root' })
 export class ChartColorizationService {
+    constructor(private colorMappingService: ColorMappingService) {}
+
     makeColor(
-        chosenColor: Record<string, string>,
+        visualizationConfig: TimeSeriesChartVisConfig,
         field: DataExplorerField,
-        groupIndex: number,
+        tag?: TagValue,
     ): string {
-        const baseColor = chosenColor[field.fullDbName + field.sourceIndex];
-        return this.adjustColorBrightness(baseColor, groupIndex);
+        const fieldKey = field.fullDbName + field.sourceIndex;
+        const baseColor = visualizationConfig.chosenColor[fieldKey];
+
+        if (!tag || tag.values.length === 0) {
+            return baseColor;
+        }
+
+        const groupKey = this.makeGroupKey(tag);
+        const fieldMappings =
+            visualizationConfig.groupedColorMappings?.[fieldKey] ?? [];
+
+        if (
+            visualizationConfig.groupedColorMode?.[fieldKey] ===
+            'custom_mapping'
+        ) {
+            return (
+                this.findMapping(fieldMappings, tag, groupKey)?.color ??
+                this.colorMappingService.getDefaultColor(groupKey)
+            );
+        }
+
+        return this.colorMappingService.getDefaultColor(
+            `${fieldKey}:${groupKey}`,
+        );
     }
 
-    private adjustColorBrightness(color: string, groupIndex: number): string {
-        const amount = groupIndex * 15;
-        let { r, g, b } = this.hexToRgb(color);
+    findLabel(
+        visualizationConfig: TimeSeriesChartVisConfig,
+        field: DataExplorerField,
+        tag?: TagValue,
+    ): string | undefined {
+        if (!tag || tag.values.length === 0) {
+            return undefined;
+        }
 
-        r = this.clampColorValue(r + amount);
-        g = this.clampColorValue(g + amount);
-        b = this.clampColorValue(b + amount);
+        const fieldKey = field.fullDbName + field.sourceIndex;
+        const fieldMappings =
+            visualizationConfig.groupedColorMappings?.[fieldKey] ?? [];
+        const mapping = this.findMapping(
+            fieldMappings,
+            tag,
+            this.makeGroupKey(tag),
+        );
 
-        return this.rgbToHex(r, g, b);
+        return mapping?.label?.trim() ? mapping.label : undefined;
     }
 
-    private hexToRgb(hex: string): { r: number; g: number; b: number } {
-        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
-        return result
-            ? {
-                  r: parseInt(result[1], 16),
-                  g: parseInt(result[2], 16),
-                  b: parseInt(result[3], 16),
-              }
-            : null;
+    private findMapping(
+        mappings: TimeSeriesGroupColorMapping[],
+        tag: TagValue,
+        groupKey: string,
+    ): TimeSeriesGroupColorMapping | undefined {
+        const candidateKeys = this.makeCandidateKeys(tag, groupKey);
+        return mappings.find(mapping => candidateKeys.includes(mapping.value));
     }
 
-    private rgbToHex(r: number, g: number, b: number): string {
-        return (
-            '#' +
-            [r, g, b]
-                .map(x => {
-                    const hex = x.toString(16);
-                    return hex.length === 1 ? '0' + hex : hex;
-                })
-                .join('')
-        );
+    private makeGroupKey(tag: TagValue): string {
+        return tag.tagKeys
+            .map((key, index) => `${key}=${tag.values[index]}`)
+            .join(', ');
     }
 
-    private clampColorValue(value: number): number {
-        return Math.max(0, Math.min(255, value));
+    private makeCandidateKeys(tag: TagValue, groupKey: string): string[] {
+        const valueStrings = tag.values.map(value => String(value));
+        const keyValuePairs = tag.tagKeys.map(
+            (key, index) => `${key}=${valueStrings[index]}`,
+        );
+
+        return [
+            groupKey,
+            valueStrings.join(', '),
+            ...keyValuePairs,
+            ...valueStrings,
+        ];
     }
 }

Reply via email to