This is an automated email from the ASF dual-hosted git repository.
dominikriemer 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 3d0406aff8 feat: Add coloring modes to time series chart in grouping
mode (#4549)
3d0406aff8 is described below
commit 3d0406aff858f17dd82f67bb827d4187466f7a83
Author: Dominik Riemer <[email protected]>
AuthorDate: Fri Jun 12 12:24:39 2026 +0200
feat: Add coloring modes to time series chart in grouping mode (#4549)
---
ui/deployment/i18n/de.json | 4 +
ui/deployment/i18n/en.json | 4 +
ui/deployment/i18n/pl.json | 4 +
.../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 ++++++++++++++++---
.../chart-container/chart-container.component.html | 7 ++
.../chart-container/chart-container.component.ts | 12 ++-
.../time-series-chart-widget-config.component.ts | 30 +++++-
.../model/time-series-chart-widget.model.ts | 10 ++
.../sp-timeseries-renderer.service.ts | 24 +++--
.../traffic-light-widget.component.html | 12 ++-
.../traffic-light-widget.component.scss | 26 +++--
.../traffic-light-widget.component.ts | 92 +++++++++++++++++-
.../models/dataview-dashboard.model.ts | 1 +
.../services/chart-colorization.service.ts | 106 ++++++++++++++-------
.../panel/dashboard-panel.component.html | 30 ++++++
.../components/panel/dashboard-panel.component.ts | 11 +++
.../edit-dashboard-dialog.component.html | 17 ++++
.../edit-dashboard-dialog.component.ts | 6 ++
21 files changed, 485 insertions(+), 111 deletions(-)
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 922fcc8568..b861858a15 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -262,6 +262,7 @@
"Current year": "Aktuelles Jahr",
"Currently configured query": "Aktuell konfigurierte Anfrage",
"Custom": "Benutzerdefiniert",
+ "Custom color mapping": "Benutzerdefiniertes Farb-Mapping",
"Custom fields": "Benutzerdefinierte Felder",
"Dark mode": "Dunkel",
"Dashboard": "Dashboard",
@@ -492,6 +493,7 @@
"Group name": "Name der Benutzergruppe",
"Group selection": "Auswahl der Gruppe",
"Grouped": "Gruppiert",
+ "Grouped colors": "Farben für Gruppierung",
"Groups": "Gruppen",
"Header": "Header",
"Header column name": "Spaltenname",
@@ -986,6 +988,7 @@
"Source": "Quelle",
"Split Number": "Anzahl der Unterteilungen",
"Spread": "Spanne",
+ "Stable palette": "Stabile Palette",
"Standard deviation": "Standardabweichung",
"Start": "Start",
"Start Adapter": "Start Adapter",
@@ -1202,6 +1205,7 @@
"We've sent out a confirmation mail to this address.": "Wir haben eine
Bestätigungsmail an diese Adresse verschickt.",
"Week": "Woche",
"Wheel": "Rad",
+ "Widget border thickness": "Rahmendicke des Widgets",
"Will be shown on the map if required by the tile server": "Wird auf der
Karte angezeigt, wenn der Tile-Server dies verlangt",
"With Threshold being": "Schwellenwert entspricht",
"X": "X",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index af22414388..175bf2e9d3 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -262,6 +262,7 @@
"Current year": null,
"Currently configured query": null,
"Custom": null,
+ "Custom color mapping": null,
"Custom fields": null,
"Dark mode": null,
"Dashboard": null,
@@ -492,6 +493,7 @@
"Group name": null,
"Group selection": null,
"Grouped": null,
+ "Grouped colors": null,
"Groups": null,
"Header": null,
"Header column name": null,
@@ -986,6 +988,7 @@
"Source": null,
"Split Number": null,
"Spread": null,
+ "Stable palette": null,
"Standard deviation": null,
"Start": null,
"Start Adapter": null,
@@ -1202,6 +1205,7 @@
"We've sent out a confirmation mail to this address.": null,
"Week": null,
"Wheel": null,
+ "Widget border thickness": null,
"Will be shown on the map if required by the tile server": null,
"With Threshold being": null,
"X": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 1f9fcbcd68..7bbc0436e1 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -262,6 +262,7 @@
"Current year": "Bieżący rok",
"Currently configured query": "Aktualnie skonfigurowane zapytanie",
"Custom": "Niestandardowe",
+ "Custom color mapping": "Własne mapowanie kolorów",
"Custom fields": "Pola niestandardowe",
"Dark mode": "Ciemny",
"Dashboard": "Pulpit",
@@ -492,6 +493,7 @@
"Group name": "Nazwa grupy użytkowników",
"Group selection": "Wybór grupy",
"Grouped": "Zgrupowane",
+ "Grouped colors": "Kolory grupowania",
"Groups": "Grupy",
"Header": "Nagłówek",
"Header column name": "Nazwa kolumny nagłówka",
@@ -986,6 +988,7 @@
"Source": "Źródło",
"Split Number": "Liczba podziałów",
"Spread": "Rozstęp",
+ "Stable palette": "Stabilna paleta",
"Standard deviation": "Odchylenie standardowe",
"Start": "Uruchom",
"Start Adapter": "Uruchom adapter",
@@ -1202,6 +1205,7 @@
"We've sent out a confirmation mail to this address.": "Wysłaliśmy wiadomość
potwierdzającą na ten adres.",
"Week": "Tydzień",
"Wheel": "Kółko",
+ "Widget border thickness": "Grubość obramowania widżetu",
"Will be shown on the map if required by the tile server": "Zostanie
pokazana na mapie, jeśli wymaga tego serwera kafelków(serwer map cyfrowych)",
"With Threshold being": "Próg wynosi",
"X": "X",
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/chart-container/chart-container.component.html
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.html
index d0ac4576b7..7f1ca237ef 100644
---
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.html
+++
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.html
@@ -22,6 +22,11 @@
background: configuredWidget.baseAppearanceConfig.backgroundColor,
color: configuredWidget.baseAppearanceConfig.textColor,
height: '100%',
+ border: !dataViewMode
+ ? (dashboardChartOverrides?.borderThickness || 0) +
+ 'px solid var(--color-bg-2)'
+ : 'none',
+ boxSizing: 'border-box',
}"
[attr.data-cy]="
'widget-' + configuredWidget.baseAppearanceConfig.widgetTitle
@@ -59,6 +64,8 @@
type="text"
[(ngModel)]="tempName"
class="name-input"
+ (keydown.enter)="saveName()"
+ (keydown.escape)="cancelEditingName()"
/>
<button mat-icon-button (click)="saveName()">
diff --git
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
index adfe34871d..5206d4f22a 100644
---
a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
+++
b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
@@ -31,6 +31,7 @@ import {
SimpleChanges,
ViewChild,
inject,
+ NgZone,
} from '@angular/core';
import {
ClientDashboardItem,
@@ -122,6 +123,7 @@ export class ChartContainerComponent
private nameChangeService = inject(NameChangeService);
private el = inject<ElementRef<HTMLDivElement>>(ElementRef);
private resizeService = inject(ResizeService);
+ private ngZone = inject(NgZone);
@ViewChild('menuTrigger') menu: MatMenuTrigger;
@ViewChild('timeSelectorMenu')
@@ -221,10 +223,12 @@ export class ChartContainerComponent
const { width, height } =
entries[entries.length - 1].contentRect;
- this.resizeService.notify({
- width,
- height,
- widgetId: this.dashboardItem?.id || undefined,
+ this.ngZone.run(() => {
+ this.resizeService.notify({
+ width,
+ height,
+ widgetId: this.dashboardItem?.id || undefined,
+ });
});
}, 100);
});
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/components/charts/traffic-light/traffic-light-widget.component.html
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.html
index e1b64c93de..0e2bbc2aae 100644
---
a/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.html
+++
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.html
@@ -39,6 +39,7 @@
<div
class="tl-container"
[ngClass]="selectedToShowValue ? 'no-border-container' : ''"
+ [style.width.px]="trafficLightShellWidth"
>
<div
class="light"
@@ -53,9 +54,16 @@
[ngClass]="{ 'light-green': activeClass === 'green' }"
></div>
</div>
- <div class="light-value-container">
+ <div
+ class="light-value-container"
+ [style.width.px]="trafficLightShellWidth"
+ >
@if (selectedToShowValue) {
- <div class="light-value text-md">
+ <div
+ class="light-value"
+ [style.fontSize.px]="trafficLightValueFontSize"
+ [style.lineHeight.px]="trafficLightValueFontSize"
+ >
{{ displayed_value }}
</div>
}
diff --git
a/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.scss
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.scss
index 160ee935a7..38c24f9ddc 100644
---
a/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.scss
+++
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.scss
@@ -20,13 +20,15 @@
}
.traffic-light-wrapper {
- display: inline-flex;
+ display: flex;
flex-direction: column;
align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
}
.tl-container {
- height: 90%;
aspect-ratio: 1 / 3;
box-sizing: border-box;
@@ -41,7 +43,7 @@
}
.no-border-container {
- border-radius: 10px 10px 0 0;
+ border-radius: 10px;
}
.light {
@@ -78,20 +80,16 @@
}
.light-value-container {
- background-color: #222;
- border-radius: 10px;
- width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
+ margin-top: 10px;
}
.light-value {
- color: #fff;
+ width: 100%;
font-weight: bold;
text-align: center;
- background-color: #222;
- padding: 10px;
-
- border-bottom-left-radius: 10px;
- border-bottom-right-radius: 10px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: clip;
}
diff --git
a/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.ts
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.ts
index 99759449e1..b7a8751f0a 100644
---
a/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.ts
+++
b/ui/src/app/chart-shared/components/charts/traffic-light/traffic-light-widget.component.ts
@@ -63,10 +63,18 @@ export class TrafficLightWidgetComponent
activeClass = 'red';
displayed_value: string;
+ currentValue?: number;
+ widgetWidth = 0;
+ widgetHeight = 0;
+ trafficLightShellWidth = 88;
+ trafficLightValueFontSize = 28;
ngOnInit(): void {
super.ngOnInit();
+ this.widgetWidth = this.initialSize.width;
+ this.widgetHeight = this.initialSize.height;
this.updateSettings();
+ this.updateTrafficLightMetrics();
}
updateSettings(): void {
@@ -84,7 +92,7 @@ export class TrafficLightWidgetComponent
getTrafficLightColor(): void {
const value = this.row[0][this.fieldIndex];
- this.displayed_value = value.toFixed(2);
+ this.currentValue = value;
if (this.isInOkRange(value)) {
this.activeClass = 'green';
@@ -93,6 +101,8 @@ export class TrafficLightWidgetComponent
} else {
this.activeClass = 'red';
}
+
+ this.updateTrafficLightMetrics();
}
exceedsThreshold(value) {
@@ -159,7 +169,11 @@ export class TrafficLightWidgetComponent
}
}
- onResize(_width: number, _heigth: number) {}
+ onResize(width: number, height: number) {
+ this.widgetWidth = width;
+ this.widgetHeight = height;
+ this.updateTrafficLightMetrics();
+ }
handleUpdatedFields(
addedFields: DataExplorerField[],
@@ -182,4 +196,78 @@ export class TrafficLightWidgetComponent
this.fieldToObserve();
this.refreshView();
}
+
+ private updateTrafficLightMetrics(): void {
+ const availableWidth = this.widgetWidth > 0 ? this.widgetWidth : 280;
+ const availableHeight = this.widgetHeight > 0 ? this.widgetHeight :
320;
+ const reservedValueHeight = this.selectedToShowValue
+ ? Math.max(36, availableHeight * 0.12)
+ : 0;
+ const usableHeight = Math.max(
+ availableHeight - reservedValueHeight,
+ 120,
+ );
+ const maxWidthByHeight = usableHeight / 3.25;
+ const preferredWidth = availableWidth * 0.4;
+ const maxShellWidth = Math.min(
+ Math.max(availableWidth * 0.55, 140),
+ 220,
+ );
+ let shellWidth = this.clamp(
+ Math.min(preferredWidth, maxWidthByHeight),
+ 72,
+ maxShellWidth,
+ );
+
+ const valueText = this.formatDisplayedValue(this.currentValue);
+ const desiredFontSize = this.clamp(
+ Math.round(shellWidth * 0.34),
+ 18,
+ 42,
+ );
+ const estimatedRequiredWidth =
+ valueText.length * desiredFontSize * 0.58 + 2 * 12;
+
+ shellWidth = Math.max(
+ shellWidth,
+ Math.min(estimatedRequiredWidth, maxShellWidth),
+ );
+ shellWidth = Math.min(
+ shellWidth,
+ availableWidth * 0.82,
+ maxWidthByHeight,
+ maxShellWidth,
+ );
+
+ this.trafficLightShellWidth = Math.round(shellWidth);
+ this.displayed_value = this.formatDisplayedValue(this.currentValue);
+ this.trafficLightValueFontSize = this.computeValueFontSize(
+ this.trafficLightShellWidth,
+ this.displayed_value,
+ );
+ }
+
+ private computeValueFontSize(width: number, value: string): number {
+ if (!value) {
+ return 28;
+ }
+
+ return this.clamp(
+ Math.floor((width - 24) / Math.max(value.length * 0.58, 1)),
+ 12,
+ 42,
+ );
+ }
+
+ private formatDisplayedValue(value: number | undefined): string {
+ if (value === undefined || value === null || Number.isNaN(value)) {
+ return '';
+ }
+
+ return value.toFixed(2);
+ }
+
+ private clamp(value: number, min: number, max: number): number {
+ return Math.max(min, Math.min(max, value));
+ }
}
diff --git a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
index 5a3c7ab498..b749ad6d95 100644
--- a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
+++ b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts
@@ -57,6 +57,7 @@ export interface BaseWidgetData<T extends
DataExplorerWidgetModel> {
export interface DashboardChartOverrides {
hideToolbox?: boolean;
+ borderThickness?: number;
}
export interface ObservableGenerator {
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,
+ ];
}
}
diff --git
a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
index bc24b024d0..86eda57b85 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
@@ -222,6 +222,36 @@
class="mt-10"
fxLayout="column"
>
+ <sp-form-field
+ [level]="3"
+ [label]="
+ 'Widget border
thickness'
+ | translate
+ "
+ >
+ <mat-form-field
+
floatLabel="auto"
+ color="accent"
+ >
+ <input
+ matInput
+
type="number"
+ min="0"
+ max="12"
+ [ngModel]="
+
dashboard
+
.dashboardGeneralSettings
+
.chartOverrides
+
.borderThickness
+ "
+
(ngModelChange)="
+
onBorderThicknessChange(
+
$event
+ )
+ "
+ />
+ </mat-form-field>
+ </sp-form-field>
<mat-checkbox
[(ngModel)]="
dashboard
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
index 0f9143b900..4f1ce4a9f9 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
@@ -301,6 +301,16 @@ export class DashboardPanelComponent
};
}
+ onBorderThicknessChange(value: number | string): void {
+ const numericValue = Number(value);
+ this.dashboard.dashboardGeneralSettings.chartOverrides = {
+ ...this.dashboard.dashboardGeneralSettings.chartOverrides,
+ borderThickness: Number.isFinite(numericValue)
+ ? Math.max(0, Math.min(12, numericValue))
+ : 0,
+ };
+ }
+
private getNextWidgetY(): number {
if (!this.dashboard?.widgets?.length) {
return 0;
@@ -675,6 +685,7 @@ export class DashboardPanelComponent
this.dashboard.dashboardGeneralSettings.globalTimeEnabled ??= true;
this.dashboard.dashboardGeneralSettings.chartOverrides ??= {};
this.dashboard.dashboardGeneralSettings.chartOverrides.hideToolbox ??=
false;
+ this.dashboard.dashboardGeneralSettings.chartOverrides.borderThickness
??= 0;
this.dashboard.dashboardGeneralSettings.gridRowHeightPx ??= 90;
}
diff --git
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
index 747ee94bd1..ff43ce6e24 100644
---
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
+++
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.html
@@ -111,6 +111,23 @@
</mat-form-field>
</sp-form-field>
<div class="mt-10" fxLayout="column">
+ <sp-form-field
+ [level]="3"
+ [label]="'Widget border thickness' | translate"
+ >
+ <mat-form-field floatLabel="auto" color="accent">
+ <input
+ matInput
+ type="number"
+ min="0"
+ max="12"
+ [(ngModel)]="
+ dashboard.dashboardGeneralSettings
+ .chartOverrides.borderThickness
+ "
+ />
+ </mat-form-field>
+ </sp-form-field>
<mat-checkbox
[(ngModel)]="
dashboard.dashboardGeneralSettings.globalTimeEnabled
diff --git
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
index 8b27be9d00..eae9b82465 100644
---
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
+++
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
@@ -116,6 +116,12 @@ export class EditDashboardDialogComponent implements
OnInit {
) {
this.dashboard.dashboardGeneralSettings.chartOverrides.hideToolbox
= false;
}
+ if (
+ this.dashboard.dashboardGeneralSettings.chartOverrides
+ .borderThickness === undefined
+ ) {
+
this.dashboard.dashboardGeneralSettings.chartOverrides.borderThickness = 0;
+ }
if (
this.dashboard.dashboardGeneralSettings.gridRowHeightPx ===
undefined