This is an automated email from the ASF dual-hosted git repository. mfholz pushed a commit to branch status-heatmap in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit d9336e7726737e81a02b601f0efac1991e69632f Author: Marcelfrueh <[email protected]> AuthorDate: Tue Mar 4 10:59:12 2025 +0100 Add status heatmap and create color mapping component --- .../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..bab151d126 --- /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</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> Add Mapping</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</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</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" + 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 35df275c26..65c4008435 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</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> Add Mapping</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</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" - 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..a302c5e918 --- /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"> + <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"> + <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 b30d640fa5..f671391244 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'; @@ -95,6 +96,7 @@ import { SpTimeseriesItemConfigComponent } from './components/chart-config/selec import { SpEchartsWidgetAppearanceConfigComponent } from './components/chart-config/echarts-widget-appearance-config/echarts-widget-appearance-config.component'; 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 { ColorMappingOptionsConfigComponent } from './components/chart-config/color-mapping-options-config/color-mapping-options-config.component'; @NgModule({ imports: [ @@ -162,6 +164,7 @@ import { SpDataZoomConfigComponent } from './components/chart-config/data-zoom-c TrafficLightWidgetConfigComponent, StatusWidgetComponent, StatusWidgetConfigComponent, + StatusHeatmapWidgetConfigComponent, MapWidgetConfigComponent, MapWidgetComponent, HeatmapWidgetConfigComponent, @@ -180,6 +183,7 @@ import { SpDataZoomConfigComponent } from './components/chart-config/data-zoom-c 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 f8ee00e76d..e864bd115b 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'; @@ -64,6 +67,7 @@ export class DataExplorerChartRegistry { constructor( private gaugeRenderer: SpGaugeRendererService, private heatmapRenderer: SpHeatmapRendererService, + private statusHeatmapRenderer: SpStatusHeatmapRendererService, private histogramRenderer: SpHistogramRendererService, private pieRenderer: SpPieRendererService, private valueHeatmapRenderer: SpValueHeatmapRendererService, @@ -115,6 +119,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: '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 {
