This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch improve-indicator-widget in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit c10a42d678daa4e8e80d99f79ac5e98603b61668 Author: Dominik Riemer <[email protected]> AuthorDate: Sun Mar 1 10:54:43 2026 +0100 feat: Improve indicator widget --- .../support/utils/dataExplorer/DataExplorerBtns.ts | 32 ++ .../tests/dataExplorer/charts/indicator.spec.ts | 20 +- .../asset-browser-filter-asset-model.component.ts | 2 - .../asset-browser-filter-labels.component.ts | 2 - .../asset-browser-filter-sites.component.ts | 2 - .../asset-browser-filter-type.component.ts | 2 - .../indicator-chart-widget-config.component.html | 31 +- .../indicator-chart-widget-config.component.ts | 18 +- .../indicator/indicator-group-card.component.html | 85 ++++ .../indicator/indicator-group-card.component.scss | 154 ++++++ .../indicator/indicator-group-card.component.ts | 154 ++++++ .../charts/indicator/indicator-renderer.service.ts | 125 ----- .../indicator/indicator-widget.component.html | 90 ++++ .../indicator/indicator-widget.component.scss | 71 +++ .../charts/indicator/indicator-widget.component.ts | 542 +++++++++++++++++++++ .../model/indicator-chart-widget.model.ts | 2 + .../table/config/table-widget-config.component.ts | 3 +- .../registry/chart-registry.service.ts | 10 +- ui/src/app/pipelines/pipelines.component.ts | 2 - 19 files changed, 1195 insertions(+), 152 deletions(-) diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts index 741f17d019..ad82b997af 100644 --- a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts +++ b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts @@ -150,6 +150,38 @@ export class DataExplorerBtns { return cy.dataCy('chart-data-preview-empty'); } + public static indicatorChart() { + return cy.dataCy('indicator-chart'); + } + + public static indicatorChartValue() { + return cy.dataCy('indicator-chart-value'); + } + + public static indicatorChartDelta() { + return cy.dataCy('indicator-chart-delta'); + } + + public static indicatorChartTitle() { + return cy.dataCy('indicator-chart-title'); + } + + public static indicatorChartDescription() { + return cy.dataCy('indicator-chart-description'); + } + + public static indicatorChartTitleInput() { + return cy.dataCy('data-explorer-indicator-title-input'); + } + + public static indicatorChartDescriptionInput() { + return cy.dataCy('data-explorer-indicator-description-input'); + } + + public static indicatorChartDeltaCheckbox() { + return cy.dataCy('data-explorer-select-delta-checkbox'); + } + public static addNewWidgetBtn() { return cy.dataCy('add-new-widget'); } diff --git a/ui/cypress/tests/dataExplorer/charts/indicator.spec.ts b/ui/cypress/tests/dataExplorer/charts/indicator.spec.ts index 3fd353f5eb..0f5716379b 100644 --- a/ui/cypress/tests/dataExplorer/charts/indicator.spec.ts +++ b/ui/cypress/tests/dataExplorer/charts/indicator.spec.ts @@ -18,6 +18,7 @@ import { DataExplorerUtils } from '../../../support/utils/dataExplorer/DataExplorerUtils'; import { PrepareTestDataUtils } from '../../../support/utils/PrepareTestDataUtils'; +import { DataExplorerBtns } from '../../../support/utils/dataExplorer/DataExplorerBtns'; describe('Test Indicator View in Data Explorer', () => { beforeEach('Setup Test', () => { @@ -31,10 +32,23 @@ describe('Test Indicator View in Data Explorer', () => { 'indicator-chart', ); - // Check checkbox DataExplorerUtils.openVisualizationConfig(); - cy.dataCy('data-explorer-select-delta-checkbox').click(); + DataExplorerBtns.indicatorChartDeltaCheckbox().click(); + DataExplorerBtns.indicatorChartTitleInput().type('Current Metric'); + DataExplorerBtns.indicatorChartDescriptionInput().type( + 'Live value compared to the previous event.', + ); - cy.dataCy('indicator-chart').should('be.visible'); + DataExplorerBtns.indicatorChart().should('be.visible'); + DataExplorerBtns.indicatorChartTitle().should( + 'contain.text', + 'Current Metric', + ); + DataExplorerBtns.indicatorChartDescription().should( + 'contain.text', + 'Live value compared to the previous event.', + ); + DataExplorerBtns.indicatorChartValue().should('not.be.empty'); + DataExplorerBtns.indicatorChartDelta().should('be.visible'); }); }); diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-asset-model/asset-browser-filter-asset-model.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-asset-model/asset-browser-filter-asset-model.component.ts index 66764f9e4d..067d59a6d0 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-asset-model/asset-browser-filter-asset-model.component.ts +++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-asset-model/asset-browser-filter-asset-model.component.ts @@ -23,7 +23,6 @@ import { AssetBrowserFilterOuterComponent } from '../asset-browser-filter-outer/ import { MatFormField } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { FormsModule } from '@angular/forms'; -import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'sp-asset-browser-filter-asset-model', @@ -35,7 +34,6 @@ import { TranslatePipe } from '@ngx-translate/core'; MatSelect, FormsModule, MatOption, - TranslatePipe, ], }) export class AssetBrowserFilterAssetModelComponent { diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-labels/asset-browser-filter-labels.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-labels/asset-browser-filter-labels.component.ts index 4c89c0a05c..1ff907baab 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-labels/asset-browser-filter-labels.component.ts +++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-labels/asset-browser-filter-labels.component.ts @@ -24,7 +24,6 @@ import { MatFormField } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { FormsModule } from '@angular/forms'; import { SpLabelComponent } from '../../../../sp-label/sp-label.component'; -import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'sp-asset-browser-filter-labels', @@ -37,7 +36,6 @@ import { TranslatePipe } from '@ngx-translate/core'; FormsModule, MatOption, SpLabelComponent, - TranslatePipe, ], }) export class AssetBrowserFilterLabelsComponent { diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-sites/asset-browser-filter-sites.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-sites/asset-browser-filter-sites.component.ts index 8c3c042ba7..a20ec0195c 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-sites/asset-browser-filter-sites.component.ts +++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-sites/asset-browser-filter-sites.component.ts @@ -23,7 +23,6 @@ import { AssetBrowserFilterOuterComponent } from '../asset-browser-filter-outer/ import { MatFormField } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { FormsModule } from '@angular/forms'; -import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'sp-asset-browser-filter-sites', @@ -35,7 +34,6 @@ import { TranslatePipe } from '@ngx-translate/core'; MatSelect, FormsModule, MatOption, - TranslatePipe, ], }) export class AssetBrowserFilterSitesComponent { diff --git a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-type/asset-browser-filter-type.component.ts b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-type/asset-browser-filter-type.component.ts index 2afb724894..8e2a518828 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-type/asset-browser-filter-type.component.ts +++ b/ui/projects/streampipes/shared-ui/src/lib/components/asset-browser/asset-browser-toolbar/asset-browser-filter/asset-browser-filter-type/asset-browser-filter-type.component.ts @@ -26,7 +26,6 @@ import { AssetBrowserFilterOuterComponent } from '../asset-browser-filter-outer/ import { MatFormField } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { FormsModule } from '@angular/forms'; -import { TranslatePipe } from '@ngx-translate/core'; @Component({ selector: 'sp-asset-browser-filter-type', @@ -38,7 +37,6 @@ import { TranslatePipe } from '@ngx-translate/core'; MatSelect, FormsModule, MatOption, - TranslatePipe, ], }) export class AssetBrowserFilterTypeComponent implements OnInit { diff --git a/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.html b/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.html index 941eec9754..97f269dbd0 100644 --- a/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.html +++ b/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.html @@ -34,11 +34,40 @@ <sp-split-section [level]="3" [title]="'Settings' | translate"> <mat-checkbox data-cy="data-explorer-select-delta-checkbox" - (change)="updateDelta($event)" + (change)="updateDelta()" [(ngModel)]=" currentlyConfiguredWidget.visualizationConfig.showDelta " >{{ 'Show delta indicator' | translate }} </mat-checkbox> </sp-split-section> + <sp-split-section [level]="3" [title]="'Content' | translate"> + <sp-form-field [level]="3" [label]="'Title' | translate"> + <mat-form-field fxFlex="100"> + <input + data-cy="data-explorer-indicator-title-input" + matInput + type="text" + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig.title + " + (ngModelChange)="triggerViewRefresh()" + /> + </mat-form-field> + </sp-form-field> + <sp-form-field [level]="3" [label]="'Description' | translate"> + <mat-form-field fxFlex="100"> + <textarea + data-cy="data-explorer-indicator-description-input" + matInput + rows="3" + [(ngModel)]=" + currentlyConfiguredWidget.visualizationConfig + .description + " + (ngModelChange)="triggerViewRefresh()" + ></textarea> + </mat-form-field> + </sp-form-field> + </sp-split-section> </sp-visualization-config-outer> diff --git a/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.ts index ee46ca678f..c86462b791 100644 --- a/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.ts +++ b/ui/src/app/chart-shared/components/charts/indicator/config/indicator-chart-widget-config.component.ts @@ -23,12 +23,18 @@ import { IndicatorChartWidgetModel, } from '../model/indicator-chart-widget.model'; import { DataExplorerField } from '@streampipes/platform-services'; -import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox'; +import { MatCheckbox } from '@angular/material/checkbox'; import { SpVisualizationConfigOuterComponent } from '../../../chart-config/visualization-config-outer/visualization-config-outer.component'; -import { SplitSectionComponent } from '@streampipes/shared-ui'; +import { + FormFieldComponent, + SplitSectionComponent, +} from '@streampipes/shared-ui'; import { SelectSinglePropertyConfigComponent } from '../../../chart-config/select-single-property-config/select-single-property-config.component'; import { FormsModule } from '@angular/forms'; import { TranslatePipe } from '@ngx-translate/core'; +import { MatFormField } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { FlexDirective } from '@ngbracket/ngx-layout/flex'; @Component({ selector: 'sp-data-explorer-indicator-chart-widget-config', @@ -36,8 +42,12 @@ import { TranslatePipe } from '@ngx-translate/core'; imports: [ SpVisualizationConfigOuterComponent, SplitSectionComponent, + FormFieldComponent, SelectSinglePropertyConfigComponent, MatCheckbox, + MatFormField, + MatInput, + FlexDirective, FormsModule, TranslatePipe, ], @@ -51,7 +61,7 @@ export class IndicatorWidgetConfigComponent extends BaseWidgetConfig< this.triggerViewRefresh(); } - updateDelta(event: MatCheckboxChange) { + updateDelta() { this.triggerViewRefresh(); } @@ -66,6 +76,8 @@ export class IndicatorWidgetConfigComponent extends BaseWidgetConfig< }, ); config.showDelta ??= false; + config.title ??= ''; + config.description ??= ''; } protected requiredFieldsForChartPresent(): boolean { diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.html b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.html new file mode 100644 index 0000000000..f3cc13e924 --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.html @@ -0,0 +1,85 @@ +<!-- + ~ 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 + class="indicator-card" + fxLayout="column" + [ngStyle]="cardStyles" + [class.grouped]="grouped" + data-cy="indicator-chart-card" +> + @if (card.label || card.detail) { + <div class="indicator-card-copy" fxLayout="column"> + @if (card.label) { + <div class="indicator-card-label">{{ card.label }}</div> + } + + @if (card.detail) { + <div class="indicator-card-detail">{{ card.detail }}</div> + } + </div> + } + + <div + class="indicator-card-value-panel" + fxLayout="column" + fxLayoutAlign="center center" + fxFlex + > + <div class="indicator-card-value" data-cy="indicator-chart-value"> + {{ card.displayValue }} + </div> + </div> + + @if (card.deltaView) { + <div + class="indicator-card-delta" + fxLayout="row" + fxLayoutAlign="start center" + [ngClass]="'indicator-card-delta-' + card.deltaView.tone" + data-cy="indicator-chart-delta" + > + <div + class="indicator-card-delta-marker" + fxLayout="row" + fxLayoutAlign="center center" + > + <mat-icon class="indicator-card-delta-icon"> + {{ card.deltaView.icon }} + </mat-icon> + </div> + <div class="indicator-card-delta-copy" fxLayout="column"> + <div class="indicator-card-delta-label"> + <span class="indicator-card-delta-value"> + {{ card.deltaView.label }} + </span> + @if (card.deltaView.detail) { + <span class="indicator-card-delta-detail"> + {{ card.deltaView.detail }} + </span> + } + </div> + @if (card.deltaView.meta) { + <div class="indicator-card-delta-meta"> + {{ card.deltaView.meta }} + </div> + } + </div> + </div> + } +</div> diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.scss b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.scss new file mode 100644 index 0000000000..a4bc0ed4db --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.scss @@ -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. + * + */ + +:host { + display: block; + height: 100%; + min-height: 0; +} + +.indicator-card { + --indicator-delta-accent: currentColor; + + background: color-mix( + in srgb, + var(--indicator-selected-background) 88%, + white + ); + border: 1px solid + color-mix(in srgb, var(--indicator-selected-background) 78%, white); + border-radius: calc(var(--space-md) * 1.2); + box-sizing: border-box; + height: 100%; + min-height: 0; + overflow: hidden; + padding: var(--indicator-card-padding); +} + +.indicator-card.grouped { + background: color-mix( + in srgb, + var(--indicator-selected-background) 86%, + white + ); +} + +.indicator-card-copy { + gap: calc(var(--indicator-card-copy-gap) * 0.4); + margin-bottom: var(--indicator-card-copy-gap); + min-height: 0; + min-width: 0; +} + +.indicator-card-label { + color: color-mix(in srgb, currentColor 82%, transparent); + font-size: var(--indicator-card-label-size); + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.1; + overflow: hidden; + overflow-wrap: anywhere; + text-wrap: balance; +} + +.indicator-card-detail { + color: color-mix(in srgb, currentColor 60%, transparent); + font-size: var(--indicator-card-detail-size); + line-height: 1.25; + overflow: hidden; + overflow-wrap: anywhere; +} + +.indicator-card-value-panel { + margin-bottom: var(--indicator-card-value-gap); + min-height: 0; + text-align: center; +} + +.indicator-card-value { + font-size: var(--indicator-card-value-size); + font-weight: 800; + letter-spacing: -0.04em; + line-height: 0.95; + max-width: 100%; + overflow-wrap: anywhere; +} + +.indicator-card-delta { + border-radius: calc(var(--space-md) * 1.1); + box-sizing: border-box; + gap: calc(var(--space-sm) * 0.8); + max-width: 100%; + margin-left: auto; + margin-right: auto; + min-height: var(--indicator-card-delta-height); + padding: var(--space-2xs) var(--space-sm) var(--space-2xs) var(--space-2xs); +} + +.indicator-card-delta-marker { + background: color-mix( + in srgb, + var(--indicator-delta-accent) 16%, + var(--color-bg-0) + ); + border-radius: 999px; + color: var(--indicator-delta-accent); + flex: 0 0 auto; + height: calc(var(--indicator-card-delta-size) * 1.55); + width: calc(var(--indicator-card-delta-size) * 1.55); +} + +.indicator-card-delta-icon { + color: inherit; + font-size: calc(var(--indicator-card-delta-size) * 0.82); + height: calc(var(--indicator-card-delta-size) * 0.82); + width: calc(var(--indicator-card-delta-size) * 0.82); +} + +.indicator-card-delta-copy { + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} + +.indicator-card-delta-label { + align-items: baseline; + display: flex; + flex-wrap: wrap; + font-size: var(--indicator-card-delta-size); + font-weight: 600; + gap: calc(var(--space-xs) * 0.8); + line-height: 0.95; + overflow: hidden; +} + +.indicator-card-delta-value { + font-weight: 700; +} + +.indicator-card-delta-detail { + font-size: calc(var(--indicator-card-delta-size) * 0.84); + font-weight: 600; +} + +.indicator-card-delta-meta { + font-size: var(--indicator-card-delta-meta-size); + line-height: 1.1; + overflow: hidden; + overflow-wrap: anywhere; +} diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.ts b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.ts new file mode 100644 index 0000000000..3de323d01c --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-group-card.component.ts @@ -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. + * + */ + +import { NgClass, NgStyle } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { + FlexDirective, + LayoutAlignDirective, + LayoutDirective, +} from '@ngbracket/ngx-layout/flex'; + +export type IndicatorDeltaTone = 'positive' | 'negative' | 'neutral'; + +export interface IndicatorDeltaView { + icon: string; + label: string; + meta?: string; + detail?: string; + tone: IndicatorDeltaTone; +} + +export interface IndicatorGroupCardView { + id: string; + label?: string; + detail?: string; + displayValue: string; + deltaView?: IndicatorDeltaView; +} + +@Component({ + selector: 'sp-indicator-group-card', + templateUrl: './indicator-group-card.component.html', + styleUrls: ['./indicator-group-card.component.scss'], + imports: [ + LayoutDirective, + LayoutAlignDirective, + FlexDirective, + NgStyle, + NgClass, + MatIcon, + ], +}) +export class IndicatorGroupCardComponent { + @Input({ required: true }) card: IndicatorGroupCardView; + @Input() cardWidth = 320; + @Input() cardHeight = 240; + @Input() grouped = false; + + get cardStyles(): Record<string, string> { + const minDimension = Math.max( + Math.min(this.cardWidth, this.cardHeight), + 1, + ); + const hasSupport = !!this.card.label || !!this.card.detail; + const hasDelta = !!this.card.deltaView; + const compactMode = this.grouped || this.cardWidth < 320; + + const padding = this.clamp( + minDimension * (compactMode ? 0.055 : 0.07), + 8, + 20, + ); + const sectionGap = this.clamp( + minDimension * (compactMode ? 0.018 : 0.028), + 4, + 12, + ); + const copyGap = this.clamp(sectionGap * 0.7, 2, 8); + const valueGap = this.clamp(sectionGap * 0.45, 2, 6); + const labelSize = this.clamp(minDimension * 0.07, 11, 18); + const detailSize = this.clamp(minDimension * 0.055, 10, 14); + const deltaSize = this.clamp( + minDimension * (compactMode ? 0.072 : 0.07), + 12, + 19, + ); + const deltaMetaSize = this.clamp( + minDimension * (compactMode ? 0.04 : 0.044), + 9, + 12, + ); + const deltaHeight = this.clamp( + minDimension * (compactMode ? 0.18 : 0.2), + 30, + 52, + ); + const supportHeight = + (this.card.label ? labelSize * 1.25 : 0) + + (this.card.detail ? detailSize * 1.35 : 0) + + (hasSupport ? copyGap : 0); + const availableValueHeight = + this.cardHeight - + padding * 2 - + supportHeight - + (hasDelta ? deltaHeight + valueGap : 0); + const valueSize = this.clamp( + Math.min( + this.cardWidth * (compactMode ? 0.19 : 0.22), + availableValueHeight * + (compactMode + ? hasSupport && hasDelta + ? 0.72 + : 0.78 + : hasSupport && hasDelta + ? 0.76 + : 0.82), + ), + compactMode ? 20 : 24, + compactMode ? 20 : 112, + ); + const deltaSizeAdjusted = this.clamp( + Math.min( + deltaSize, + this.cardWidth * (compactMode ? 0.072 : 0.082), + deltaHeight * (compactMode ? 0.32 : 0.36), + ), + 10, + 18, + ); + + return { + '--indicator-card-padding': `${padding}px`, + '--indicator-card-gap': `${sectionGap}px`, + '--indicator-card-copy-gap': `${copyGap}px`, + '--indicator-card-value-gap': `${valueGap}px`, + '--indicator-card-label-size': `${labelSize}px`, + '--indicator-card-detail-size': `${detailSize}px`, + '--indicator-card-value-size': `${valueSize}px`, + '--indicator-card-delta-size': `${deltaSizeAdjusted}px`, + '--indicator-card-delta-meta-size': `${deltaMetaSize}px`, + '--indicator-card-delta-height': `${deltaHeight}px`, + }; + } + + private clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); + } +} diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-renderer.service.ts b/ui/src/app/chart-shared/components/charts/indicator/indicator-renderer.service.ts deleted file mode 100644 index 9c19c9b4f2..0000000000 --- a/ui/src/app/chart-shared/components/charts/indicator/indicator-renderer.service.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 { Injectable } from '@angular/core'; -import { SpBaseEchartsRenderer } from '../../../echarts-renderer/base-echarts-renderer'; -import { IndicatorChartWidgetModel } from './model/indicator-chart-widget.model'; -import { GeneratedDataset, WidgetSize } from '../../../models/dataset.model'; -import { EChartsOption, GraphicComponentOption } from 'echarts'; -import { FieldUpdateInfo } from '../../../models/field-update.model'; - -@Injectable({ providedIn: 'root' }) -export class SpIndicatorRendererService extends SpBaseEchartsRenderer<IndicatorChartWidgetModel> { - applyOptions( - generatedDataset: GeneratedDataset, - options: EChartsOption, - widgetConfig: IndicatorChartWidgetModel, - widgetSize: WidgetSize, - ): void { - const field = widgetConfig.visualizationConfig.valueField; - const datasetOption = this.datasetUtilsService.findPreparedDataset( - generatedDataset, - field.sourceIndex, - ).rawDataset; - const fieldIndex = datasetOption.dimensions.indexOf(field.fullDbName); - const datasetSize = datasetOption.source.length as number; - const value = (datasetOption.source as any)[datasetSize - 1][ - fieldIndex - ]; - const graphicElements: GraphicComponentOption[] = []; - let previousValue = undefined; - graphicElements.push(this.makeCurrentValue(value, widgetConfig)); - if (datasetSize > 1 && widgetConfig.visualizationConfig.showDelta) { - previousValue = (datasetOption.source as any)[datasetSize - 2][ - fieldIndex - ]; - graphicElements.push( - this.makeDelta(value, previousValue, widgetConfig), - ); - } - - Object.assign(options, { - graphic: { - elements: graphicElements, - }, - }); - } - - public handleUpdatedFields( - fieldUpdateInfo: FieldUpdateInfo, - widgetConfig: IndicatorChartWidgetModel, - ): void { - widgetConfig.visualizationConfig.valueField = - this.fieldUpdateService.updateSingleField( - widgetConfig.visualizationConfig.valueField, - fieldUpdateInfo.fieldProvider.allFields, - fieldUpdateInfo, - field => true, - ); - } - - makeCurrentValue( - value: any, - widgetConfig: IndicatorChartWidgetModel, - ): GraphicComponentOption { - return { - type: 'text', - left: 'center', - top: '30%', - style: this.makeTextStyle(value, 80, widgetConfig), - }; - } - - makeDelta( - value: any, - previousValue: any, - widgetConfig: IndicatorChartWidgetModel, - ): GraphicComponentOption { - const delta = value - previousValue; - return { - type: 'text', - left: 'center', - top: '50%', - style: this.makeTextStyle(delta, 50, widgetConfig), - }; - } - - makeTextStyle( - textContent: any, - fontSize: number, - widgetConfig: IndicatorChartWidgetModel, - ) { - return { - text: this.formatValue(textContent), - fontSize, - fontWeight: 'bold', - lineDash: [0, 200], - lineDashOffset: 0, - fill: widgetConfig.baseAppearanceConfig.textColor, - lineWidth: 1, - }; - } - - formatValue(value: any): any { - if (typeof value === 'number') { - return parseFloat(value.toFixed(3)); - } else { - return value; - } - } -} diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.html b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.html new file mode 100644 index 0000000000..3d841a182d --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.html @@ -0,0 +1,90 @@ +<!-- + ~ 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 + class="indicator-widget" + fxLayout="column" + fxFlex="100" + [ngStyle]="widgetStyles" + data-cy="indicator-chart" +> + @if (showNoDataInDateRange) { + <sp-no-data-in-date-range [viewDateRange]="timeSettings" class="h-100"> + </sp-no-data-in-date-range> + } + + @if (showTooMuchData) { + <sp-too-much-data + [amountOfEvents]="amountOfTooMuchEvents" + (loadDataWithTooManyEventsEmitter)="loadDataWithTooManyEvents()" + class="h-100" + > + </sp-too-much-data> + } + + @if (showInvalidConfiguration) { + <sp-invalid-configuration + [widgetTypeLabel]="widgetTypeLabel" + class="h-100" + > + </sp-invalid-configuration> + } + + @if (showData) { + <div class="indicator-shell" fxLayout="column" fxFlex="100"> + @if (titleText || descriptionText) { + <div class="indicator-copy" fxLayout="column"> + @if (titleText) { + <div + class="indicator-title" + data-cy="indicator-chart-title" + > + {{ titleText }} + </div> + } + + @if (descriptionText) { + <div + class="indicator-description" + data-cy="indicator-chart-description" + > + {{ descriptionText }} + </div> + } + </div> + } + + <div + class="indicator-grid" + fxFlex + [class.grouped]="hasMultipleCards" + [ngStyle]="gridStyles" + > + @for (card of indicatorCards; track trackCard($index, card)) { + <sp-indicator-group-card + [card]="card" + [cardWidth]="cardWidth" + [cardHeight]="cardHeight" + [grouped]="hasMultipleCards" + > + </sp-indicator-group-card> + } + </div> + </div> + } +</div> diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.scss b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.scss new file mode 100644 index 0000000000..1036ea7710 --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.scss @@ -0,0 +1,71 @@ +/* + * 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. + * + */ + +:host { + display: block; + height: 100%; +} + +.h-100 { + height: 100%; +} + +.indicator-widget { + box-sizing: border-box; + height: 100%; + min-height: 0; + overflow: hidden; + padding: var(--indicator-padding); +} + +.indicator-shell { + gap: var(--indicator-gap); + min-height: 0; +} + +.indicator-copy { + gap: calc(var(--indicator-gap) * 0.45); +} + +.indicator-title { + font-size: var(--indicator-title-size); + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.1; +} + +.indicator-description { + color: color-mix(in srgb, currentColor 72%, transparent); + font-size: var(--indicator-description-size); + line-height: 1.35; + max-width: 42ch; +} + +.indicator-grid { + display: grid; + gap: var(--indicator-gap); + grid-template-columns: repeat( + var(--indicator-grid-columns), + minmax(0, 1fr) + ); + min-height: 0; +} + +.indicator-grid.grouped { + align-content: start; +} diff --git a/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.ts b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.ts new file mode 100644 index 0000000000..84b843a036 --- /dev/null +++ b/ui/src/app/chart-shared/components/charts/indicator/indicator-widget.component.ts @@ -0,0 +1,542 @@ +/* + * 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 { NgStyle } from '@angular/common'; +import { Component, inject, LOCALE_ID, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex'; +import { + DataExplorerField, + DataSeries, + SpQueryResult, +} from '@streampipes/platform-services'; +import { BaseDataExplorerWidgetDirective } from '../base/base-data-explorer-widget.directive'; +import { SpInvalidConfigurationComponent } from '../base/invalid-configuration/invalid-configuration.component'; +import { NoDataInDateRangeComponent } from '../base/no-data/no-data-in-date-range.component'; +import { TooMuchDataComponent } from '../base/too-much-data/too-much-data.component'; +import { + IndicatorDeltaView, + IndicatorGroupCardComponent, + IndicatorGroupCardView, +} from './indicator-group-card.component'; +import { IndicatorChartWidgetModel } from './model/indicator-chart-widget.model'; + +@Component({ + selector: 'sp-data-explorer-indicator-widget', + templateUrl: './indicator-widget.component.html', + styleUrls: ['./indicator-widget.component.scss'], + imports: [ + LayoutDirective, + FlexDirective, + NgStyle, + NoDataInDateRangeComponent, + TooMuchDataComponent, + SpInvalidConfigurationComponent, + IndicatorGroupCardComponent, + ], +}) +export class IndicatorWidgetComponent + extends BaseDataExplorerWidgetDirective<IndicatorChartWidgetModel> + implements OnInit +{ + indicatorCards: IndicatorGroupCardView[] = []; + widgetTypeLabel: string; + + private hasReceivedData = false; + private latestData: SpQueryResult[] = []; + private readonly locale = inject(LOCALE_ID); + private readonly translateService = inject(TranslateService); + + ngOnInit(): void { + super.ngOnInit(); + this.widgetTypeLabel = this.widgetRegistryService.getChartTemplate( + this.dataExplorerWidget.widgetType, + ).label; + } + + get widgetStyles(): Record<string, string> { + const minDimension = Math.max( + Math.min(this.currentWidth ?? 0, this.currentHeight ?? 0), + 1, + ); + + return { + 'background': + this.dataExplorerWidget.baseAppearanceConfig.backgroundColor, + 'color': this.dataExplorerWidget.baseAppearanceConfig.textColor, + '--indicator-selected-background': + this.dataExplorerWidget.baseAppearanceConfig.backgroundColor, + '--indicator-padding': `${this.clamp( + minDimension * 0.08, + 12, + 28, + )}px`, + '--indicator-gap': `${this.clamp(minDimension * 0.045, 8, 20)}px`, + '--indicator-title-size': `${this.clamp( + minDimension * 0.082, + 14, + 30, + )}px`, + '--indicator-description-size': `${this.clamp( + minDimension * 0.055, + 12, + 18, + )}px`, + }; + } + + get gridStyles(): Record<string, string> { + return { + '--indicator-grid-columns': `${this.gridColumnCount}`, + }; + } + + get titleText(): string { + return this.dataExplorerWidget.visualizationConfig.title?.trim() ?? ''; + } + + get descriptionText(): string { + return ( + this.dataExplorerWidget.visualizationConfig.description?.trim() ?? + '' + ); + } + + get hasMultipleCards(): boolean { + return this.indicatorCards.length > 1; + } + + get cardWidth(): number { + const width = this.currentWidth ?? 0; + const gap = this.estimatedGap; + return Math.max( + (width - gap * Math.max(this.gridColumnCount - 1, 0)) / + this.gridColumnCount, + 180, + ); + } + + get cardHeight(): number { + const availableHeight = + (this.currentHeight ?? 0) - + this.estimatedCopyHeight - + this.estimatedPadding * 2 - + this.estimatedGap * Math.max(this.gridRowCount - 1, 0); + + return Math.max( + availableHeight / this.gridRowCount, + this.hasMultipleCards ? 92 : 120, + ); + } + + beforeDataFetched(): void { + this.setShownComponents(false, false, true, false); + } + + onDataReceived(spQueryResults: SpQueryResult[]): void { + this.hasReceivedData = true; + this.latestData = spQueryResults; + this.updateIndicator(); + } + + onResize(_width: number, _height: number): void { + this.refreshView(); + } + + refreshView(): void { + this.updateIndicator(); + } + + handleUpdatedFields( + addedFields: DataExplorerField[], + removedFields: DataExplorerField[], + ): void { + const fieldUpdateInfo = { + addedFields, + removedFields, + fieldProvider: this.fieldProvider, + }; + + this.dataExplorerWidget.visualizationConfig.valueField = + this.fieldUpdateService.updateSingleField( + this.dataExplorerWidget.visualizationConfig.valueField, + fieldUpdateInfo.fieldProvider.allFields, + fieldUpdateInfo, + () => true, + ); + this.dataExplorerWidget.visualizationConfig.deltaField = + this.fieldUpdateService.updateSingleField( + this.dataExplorerWidget.visualizationConfig.deltaField, + fieldUpdateInfo.fieldProvider.allFields, + fieldUpdateInfo, + () => true, + ); + this.refreshView(); + } + + trackCard(_index: number, card: IndicatorGroupCardView): string { + return card.id; + } + + private updateIndicator(): void { + if ( + this.dataExplorerWidget.visualizationConfig.configurationValid === + false + ) { + this.showInvalidConfiguration = true; + this.indicatorCards = []; + this.setShownComponents(false, false, false, false); + return; + } + + this.showInvalidConfiguration = false; + if (!this.hasReceivedData && this.latestData.length === 0) { + return; + } + + const valueField = + this.dataExplorerWidget.visualizationConfig.valueField; + this.indicatorCards = this.buildCards(valueField); + + if (this.indicatorCards.length === 0) { + this.setShownComponents(true, false, false, false); + return; + } + + this.setShownComponents(false, true, false, false); + } + + private buildCards( + valueField: DataExplorerField | undefined, + ): IndicatorGroupCardView[] { + if (!valueField) { + return []; + } + + const result = this.findQueryResult(valueField.sourceIndex); + if (!result) { + return []; + } + + const seriesList = result?.allDataSeries ?? []; + + return seriesList + .map((series, index) => + this.createCard(valueField, result, series, index), + ) + .filter( + (card): card is IndicatorGroupCardView => card !== undefined, + ); + } + + private createCard( + valueField: DataExplorerField, + result: SpQueryResult, + series: DataSeries, + index: number, + ): IndicatorGroupCardView | undefined { + const currentValue = this.getSeriesFieldValue( + result, + series, + valueField, + 0, + ); + + if (currentValue === undefined) { + return undefined; + } + + const groupInfo = this.makeGroupInfo(series.tags ?? {}); + + return { + id: `${result.sourceIndex}-${index}-${this.makeTagSignature( + series.tags ?? {}, + )}`, + label: groupInfo.label, + detail: groupInfo.detail, + displayValue: this.formatValue(currentValue), + deltaView: this.buildDelta(valueField, series, currentValue), + }; + } + + private buildDelta( + valueField: DataExplorerField, + series: DataSeries, + currentValue: unknown, + ): IndicatorDeltaView | undefined { + if (!this.dataExplorerWidget.visualizationConfig.showDelta) { + return undefined; + } + + const deltaField = + this.dataExplorerWidget.visualizationConfig.deltaField; + const referenceValue = deltaField + ? this.getMatchingGroupValue(deltaField, series.tags ?? {}) + : this.getSeriesFieldValue( + this.findQueryResult(valueField.sourceIndex), + series, + valueField, + 1, + ); + + if (referenceValue === undefined) { + return undefined; + } + + const referenceLabel = deltaField ? deltaField.runtimeName : undefined; + + if ( + typeof currentValue === 'number' && + typeof referenceValue === 'number' + ) { + const delta = this.normalizeNumericDelta( + currentValue - referenceValue, + ); + const percentDelta = + referenceValue === 0 + ? undefined + : this.normalizeNumericDelta( + delta / Math.abs(referenceValue), + ); + + return { + icon: + delta > 0 + ? 'trending_up' + : delta < 0 + ? 'trending_down' + : 'trending_flat', + label: this.formatSignedNumber(delta), + detail: + percentDelta !== undefined + ? this.formatSignedPercent(percentDelta) + : undefined, + meta: referenceLabel, + tone: + delta > 0 ? 'positive' : delta < 0 ? 'negative' : 'neutral', + }; + } + + const changed = currentValue !== referenceValue; + + return { + icon: changed ? 'compare_arrows' : 'horizontal_rule', + label: this.translateService.instant( + changed ? 'Changed' : 'No change', + ), + meta: referenceLabel, + tone: 'neutral', + }; + } + + private getMatchingGroupValue( + field: DataExplorerField, + tags: Record<string, string>, + ): unknown | undefined { + const result = this.findQueryResult(field.sourceIndex); + if (!result) { + return undefined; + } + + const matchingSeries = this.findMatchingSeries(result, tags); + return this.getSeriesFieldValue(result, matchingSeries, field, 0); + } + + private findMatchingSeries( + result: SpQueryResult, + tags: Record<string, string>, + ): DataSeries | undefined { + const targetSignature = this.makeTagSignature(tags); + return ( + result.allDataSeries.find( + series => + this.makeTagSignature(series.tags ?? {}) === + targetSignature, + ) ?? + (result.allDataSeries.length === 1 + ? result.allDataSeries[0] + : undefined) + ); + } + + private findQueryResult(sourceIndex: number): SpQueryResult | undefined { + return ( + this.latestData.find(item => item.sourceIndex === sourceIndex) ?? + this.latestData[sourceIndex] + ); + } + + private getSeriesFieldValue( + result: SpQueryResult | undefined, + series: DataSeries | undefined, + field: DataExplorerField, + rowIndex: number, + ): unknown | undefined { + const row = series?.rows?.[rowIndex]; + if (!row) { + return undefined; + } + + const fieldIndex = this.findFieldIndex( + result?.headers ?? series.headers, + field, + ); + + return fieldIndex >= 0 ? row[fieldIndex] : undefined; + } + + private findFieldIndex( + headers: string[] | undefined, + field: DataExplorerField, + ): number { + if (!headers) { + return -1; + } + + return headers.findIndex( + header => + header === field.fullDbName || header === field.runtimeName, + ); + } + + private makeGroupInfo(tags: Record<string, string>): { + label?: string; + detail?: string; + } { + const entries = Object.entries(tags); + + if (entries.length === 0) { + return {}; + } + + if (entries.length === 1) { + const [key, value] = entries[0]; + return { + label: value, + detail: key, + }; + } + + return { + label: entries + .map(([key, value]) => `${key}: ${value}`) + .join(' · '), + }; + } + + private makeTagSignature(tags: Record<string, string>): string { + return JSON.stringify( + Object.entries(tags).sort(([left], [right]) => + left.localeCompare(right), + ), + ); + } + + private formatValue(value: unknown): string { + if (typeof value === 'number') { + return new Intl.NumberFormat(this.locale, { + maximumFractionDigits: 3, + }).format(value); + } + + if (typeof value === 'boolean') { + return this.translateService.instant(value ? 'True' : 'False'); + } + + if (value === null || value === undefined || value === '') { + return '—'; + } + + return `${value}`; + } + + private formatSignedNumber(value: number): string { + return new Intl.NumberFormat(this.locale, { + maximumFractionDigits: 3, + signDisplay: 'exceptZero', + }).format(value); + } + + private formatSignedPercent(value: number): string { + return new Intl.NumberFormat(this.locale, { + style: 'percent', + maximumFractionDigits: 1, + signDisplay: 'exceptZero', + }).format(value); + } + + private normalizeNumericDelta(value: number): number { + return Math.abs(value) < 0.000_000_1 ? 0 : value; + } + + private get gridColumnCount(): number { + if (this.indicatorCards.length <= 1) { + return 1; + } + + return Math.min( + this.indicatorCards.length, + Math.max(1, Math.floor((this.currentWidth ?? 0) / 220)), + ); + } + + private get gridRowCount(): number { + return Math.max( + 1, + Math.ceil(this.indicatorCards.length / this.gridColumnCount), + ); + } + + private get estimatedPadding(): number { + const minDimension = Math.max( + Math.min(this.currentWidth ?? 0, this.currentHeight ?? 0), + 1, + ); + return this.clamp(minDimension * 0.08, 12, 28); + } + + private get estimatedGap(): number { + const minDimension = Math.max( + Math.min(this.currentWidth ?? 0, this.currentHeight ?? 0), + 1, + ); + return this.clamp(minDimension * 0.045, 8, 20); + } + + private get estimatedCopyHeight(): number { + let height = 0; + + if (this.titleText) { + height += this.clamp((this.currentHeight ?? 0) * 0.11, 28, 52); + } + + if (this.descriptionText) { + height += this.clamp((this.currentHeight ?? 0) * 0.1, 24, 56); + } + + if (height > 0) { + height += this.estimatedGap; + } + + return height; + } + + private clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); + } +} diff --git a/ui/src/app/chart-shared/components/charts/indicator/model/indicator-chart-widget.model.ts b/ui/src/app/chart-shared/components/charts/indicator/model/indicator-chart-widget.model.ts index a43726f407..ce8589c1aa 100644 --- a/ui/src/app/chart-shared/components/charts/indicator/model/indicator-chart-widget.model.ts +++ b/ui/src/app/chart-shared/components/charts/indicator/model/indicator-chart-widget.model.ts @@ -27,6 +27,8 @@ export interface IndicatorChartVisConfig extends DataExplorerVisConfig { valueField?: DataExplorerField; deltaField?: DataExplorerField; showDelta?: boolean; + title?: string; + description?: string; } export interface IndicatorChartWidgetModel extends DataExplorerWidgetModel { diff --git a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts index d374a7fc31..2b311bdfde 100644 --- a/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts +++ b/ui/src/app/chart-shared/components/charts/table/config/table-widget-config.component.ts @@ -29,7 +29,7 @@ import { SpAlertBannerComponent, SplitSectionComponent, } from '@streampipes/shared-ui'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatFormField } from '@angular/material/form-field'; import { FlexDirective, LayoutAlignDirective, @@ -52,7 +52,6 @@ import { MatCheckbox } from '@angular/material/checkbox'; SplitSectionComponent, MatFormField, FlexDirective, - MatLabel, MatInput, FormsModule, MatSelect, diff --git a/ui/src/app/chart-shared/registry/chart-registry.service.ts b/ui/src/app/chart-shared/registry/chart-registry.service.ts index a114037ad1..6d6103b417 100644 --- a/ui/src/app/chart-shared/registry/chart-registry.service.ts +++ b/ui/src/app/chart-shared/registry/chart-registry.service.ts @@ -46,8 +46,6 @@ import { SpValueHeatmapRendererService } from '../components/charts/value-heatma import { CorrelationChartWidgetModel } from '../components/charts/correlation-chart/model/correlation-chart-widget.model'; import { SpScatterRendererService } from '../components/charts/scatter/scatter-renderer.service'; import { SpDensityRendererService } from '../components/charts/density/density-renderer.service'; -import { IndicatorChartWidgetModel } from '../components/charts/indicator/model/indicator-chart-widget.model'; -import { SpIndicatorRendererService } from '../components/charts/indicator/indicator-renderer.service'; import { TimeSeriesChartWidgetModel } from '../components/charts/time-series-chart/model/time-series-chart-widget.model'; import { SpTimeseriesRendererService } from '../components/charts/time-series-chart/sp-timeseries-renderer.service'; import { SpEchartsWidgetAppearanceConfigComponent } from '../components/chart-config/echarts-widget-appearance-config/echarts-widget-appearance-config.component'; @@ -59,6 +57,7 @@ import { TrafficLightWidgetConfigComponent } from '../components/charts/traffic- import { TrafficLightWidgetComponent } from '../components/charts/traffic-light/traffic-light-widget.component'; import { StatusWidgetConfigComponent } from '../components/charts/status/config/status-widget-config.component'; import { StatusWidgetComponent } from '../components/charts/status/status-widget.component'; +import { IndicatorWidgetComponent } from '../components/charts/indicator/indicator-widget.component'; import { TranslateService } from '@ngx-translate/core'; @Injectable({ providedIn: 'root' }) @@ -74,7 +73,6 @@ export class ChartRegistry { private valueHeatmapRenderer: SpValueHeatmapRendererService, private scatterRenderer: SpScatterRendererService, private densityRenderer: SpDensityRendererService, - private indicatorRenderer: SpIndicatorRendererService, private timeseriesRenderer: SpTimeseriesRendererService, private translateService: TranslateService, ) { @@ -186,12 +184,8 @@ export class ChartRegistry { { id: 'indicator-chart', label: this.translateService.instant('Indicator'), - widgetAppearanceConfigurationComponent: - SpEchartsWidgetAppearanceConfigComponent, widgetConfigurationComponent: IndicatorWidgetConfigComponent, - widgetComponent: - SpEchartsWidgetComponent<IndicatorChartWidgetModel>, - chartRenderer: this.indicatorRenderer, + widgetComponent: IndicatorWidgetComponent, icon: '123', description: this.translateService.instant( 'The current value displayed as a number', diff --git a/ui/src/app/pipelines/pipelines.component.ts b/ui/src/app/pipelines/pipelines.component.ts index 3d2dae8ac8..40269529ba 100644 --- a/ui/src/app/pipelines/pipelines.component.ts +++ b/ui/src/app/pipelines/pipelines.component.ts @@ -48,7 +48,6 @@ import { LayoutGapDirective, } from '@ngbracket/ngx-layout/flex'; import { MatButton, MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { PipelineOverviewComponent } from './components/pipeline-overview/pipeline-overview.component'; import { FunctionsOverviewComponent } from './components/functions-overview/functions-overview.component'; @@ -65,7 +64,6 @@ import { TranslatePipe } from '@ngx-translate/core'; LayoutDirective, LayoutGapDirective, MatButton, - MatIcon, MatIconButton, MatTooltip, SpBasicHeaderTitleComponent,
