@meshmakers/octo-meshboard 3.5.530 → 3.5.560
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
|
|
1
|
+
import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, OCTO_SILENT_ERRORS, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
|
|
2
2
|
export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, input, Directive, model, computed, Injector, EnvironmentInjector, ApplicationRef, signal, effect, untracked, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer,
|
|
4
|
+
import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, input, Directive, model, computed, Injector, EnvironmentInjector, ApplicationRef, signal, effect, untracked, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, ChangeDetectorRef, ViewChildren, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, output, viewChild } from '@angular/core';
|
|
5
5
|
import * as i1$1 from '@angular/forms';
|
|
6
6
|
import { NG_VALUE_ACCESSOR, FormsModule, NG_VALIDATORS } from '@angular/forms';
|
|
7
7
|
import { firstValueFrom, from, map, Observable, of, switchMap as switchMap$1, forkJoin, catchError, NEVER, interval, Subject, BehaviorSubject, filter as filter$1, combineLatest, defer, EMPTY } from 'rxjs';
|
|
@@ -16,7 +16,7 @@ import * as i2 from '@progress/kendo-angular-buttons';
|
|
|
16
16
|
import { ButtonsModule, ButtonModule } from '@progress/kendo-angular-buttons';
|
|
17
17
|
import * as i3 from '@progress/kendo-angular-inputs';
|
|
18
18
|
import { InputsModule, CheckBoxModule, NumericTextBoxModule, TextBoxModule, FormFieldModule } from '@progress/kendo-angular-inputs';
|
|
19
|
-
import { ExpressionEvaluatorService, GetProcessDiagramDtoGQL, GetProcessDiagramsDtoGQL, CreateProcessDiagramDtoGQL, UpdateProcessDiagramDtoGQL, SymbolLibraryService, renderAnimations, getFlowParticlesAnimation, renderFlowParticles } from '@meshmakers/octo-process-diagrams';
|
|
19
|
+
import { ExpressionEvaluatorService, GetProcessDiagramDtoGQL, GetProcessDiagramsDtoGQL, CreateProcessDiagramDtoGQL, UpdateProcessDiagramDtoGQL, SymbolLibraryService, renderAnimations, getFlowParticlesAnimation, renderFlowParticles, evaluateTextValue, applyTextValueToPrimitive } from '@meshmakers/octo-process-diagrams';
|
|
20
20
|
import * as i1$5 from '@progress/kendo-angular-dialog';
|
|
21
21
|
import { WindowService, WindowCloseResult, WindowRef, DialogsModule, DialogRef, DialogModule, DialogService } from '@progress/kendo-angular-dialog';
|
|
22
22
|
import { switchMap, map as map$1, catchError as catchError$1, take, debounceTime, filter, startWith, shareReplay, tap } from 'rxjs/operators';
|
|
@@ -645,6 +645,9 @@ class PersistentQueryDialogDataSource {
|
|
|
645
645
|
rtId: item.rtId,
|
|
646
646
|
name: item.name ?? '',
|
|
647
647
|
description: item.description,
|
|
648
|
+
// The family filter classifies by the query entity's own CK type (AB#7951):
|
|
649
|
+
// without it every item counts as unknown and a stream-data-only dialog lists nothing.
|
|
650
|
+
ckTypeId: item.ckTypeId,
|
|
648
651
|
queryCkTypeId: item.queryCkTypeId
|
|
649
652
|
}));
|
|
650
653
|
const items = filterByFamily(rawItems, this.acceptFamilies);
|
|
@@ -1539,9 +1542,26 @@ class FormulaFieldComponent {
|
|
|
1539
1542
|
...(ngDevMode ? [{ debugName: "clampHint" }] : /* istanbul ignore next */ []));
|
|
1540
1543
|
label = input('Formula', /* @ts-ignore */
|
|
1541
1544
|
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1545
|
+
/**
|
|
1546
|
+
* Show the live preview (default). Chart formula series (AB#7952) turn it off: their operands
|
|
1547
|
+
* have one value per bucket, so a single preview value means nothing.
|
|
1548
|
+
*/
|
|
1549
|
+
showPreview = input(true, /* @ts-ignore */
|
|
1550
|
+
...(ngDevMode ? [{ debugName: "showPreview" }] : /* istanbul ignore next */ []));
|
|
1551
|
+
/** Heading of the insertable names (chart formula series: "Available series"). */
|
|
1552
|
+
variablesLabel = input('Available Variables', /* @ts-ignore */
|
|
1553
|
+
...(ngDevMode ? [{ debugName: "variablesLabel" }] : /* istanbul ignore next */ []));
|
|
1554
|
+
/** What the operands are called in the hint ("variables" / "series"). */
|
|
1555
|
+
namesNoun = input('variables', /* @ts-ignore */
|
|
1556
|
+
...(ngDevMode ? [{ debugName: "namesNoun" }] : /* istanbul ignore next */ []));
|
|
1557
|
+
/** Reference syntax shown in the hint. */
|
|
1558
|
+
syntaxHint = input('${variableName}', /* @ts-ignore */
|
|
1559
|
+
...(ngDevMode ? [{ debugName: "syntaxHint" }] : /* istanbul ignore next */ []));
|
|
1560
|
+
/** Text shown when there is nothing to insert. */
|
|
1561
|
+
emptyVariablesText = input('No variables defined. Add variables in the MeshBoard settings or publish a widget value as output variable.', /* @ts-ignore */
|
|
1562
|
+
...(ngDevMode ? [{ debugName: "emptyVariablesText" }] : /* istanbul ignore next */ []));
|
|
1542
1563
|
placeholder = input('e.g., (${a} - ${b}) / 1000', /* @ts-ignore */
|
|
1543
1564
|
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1544
|
-
variableSyntaxHint = '${variableName}';
|
|
1545
1565
|
clampExample = 'max(0, min(100, …))';
|
|
1546
1566
|
id = nextFormulaFieldId++;
|
|
1547
1567
|
inputId = `mm-formula-field-${this.id}`;
|
|
@@ -1574,7 +1594,7 @@ class FormulaFieldComponent {
|
|
|
1574
1594
|
this.formula.set(`${formula}${separator}\${${name}}`);
|
|
1575
1595
|
}
|
|
1576
1596
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: FormulaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1577
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: FormulaFieldComponent, isStandalone: true, selector: "mm-meshboard-formula-field", inputs: { formula: { classPropertyName: "formula", publicName: "formula", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, declaredNames: { classPropertyName: "declaredNames", publicName: "declaredNames", isSignal: true, isRequired: false, transformFunction: null }, numericOnly: { classPropertyName: "numericOnly", publicName: "numericOnly", isSignal: true, isRequired: false, transformFunction: null }, clampHint: { classPropertyName: "clampHint", publicName: "clampHint", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formula: "formulaChange" }, ngImport: i0, template: `
|
|
1597
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: FormulaFieldComponent, isStandalone: true, selector: "mm-meshboard-formula-field", inputs: { formula: { classPropertyName: "formula", publicName: "formula", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, declaredNames: { classPropertyName: "declaredNames", publicName: "declaredNames", isSignal: true, isRequired: false, transformFunction: null }, numericOnly: { classPropertyName: "numericOnly", publicName: "numericOnly", isSignal: true, isRequired: false, transformFunction: null }, clampHint: { classPropertyName: "clampHint", publicName: "clampHint", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, variablesLabel: { classPropertyName: "variablesLabel", publicName: "variablesLabel", isSignal: true, isRequired: false, transformFunction: null }, namesNoun: { classPropertyName: "namesNoun", publicName: "namesNoun", isSignal: true, isRequired: false, transformFunction: null }, syntaxHint: { classPropertyName: "syntaxHint", publicName: "syntaxHint", isSignal: true, isRequired: false, transformFunction: null }, emptyVariablesText: { classPropertyName: "emptyVariablesText", publicName: "emptyVariablesText", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formula: "formulaChange" }, ngImport: i0, template: `
|
|
1578
1598
|
<div class="form-field">
|
|
1579
1599
|
<label [attr.for]="inputId">{{ label() }} <span class="required" aria-hidden="true">*</span></label>
|
|
1580
1600
|
<kendo-textbox
|
|
@@ -1586,7 +1606,7 @@ class FormulaFieldComponent {
|
|
|
1586
1606
|
[inputAttributes]="inputAttributes()">
|
|
1587
1607
|
</kendo-textbox>
|
|
1588
1608
|
<p class="field-hint" [id]="hintId">
|
|
1589
|
-
Reference
|
|
1609
|
+
Reference {{ namesNoun() }} with <code>{{ syntaxHint() }}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
|
|
1590
1610
|
<code>min</code>, <code>max</code>, <code>abs</code>, <code>round</code>, <code>clamp</code>, <code>? :</code>.
|
|
1591
1611
|
Numbers use a dot as decimal separator.
|
|
1592
1612
|
@if (clampHint()) {
|
|
@@ -1595,24 +1615,24 @@ class FormulaFieldComponent {
|
|
|
1595
1615
|
</p>
|
|
1596
1616
|
@if (formula().trim() && validation().error) {
|
|
1597
1617
|
<p class="field-error" [id]="messageId" role="alert">{{ validation().error }}</p>
|
|
1598
|
-
} @else if (formula().trim()) {
|
|
1618
|
+
} @else if (formula().trim() && showPreview()) {
|
|
1599
1619
|
<p class="field-hint" [id]="messageId" aria-live="polite">Preview: <strong>{{ preview() }}</strong></p>
|
|
1600
1620
|
}
|
|
1601
1621
|
</div>
|
|
1602
1622
|
<div class="form-field">
|
|
1603
|
-
<span class="field-label" [id]="variablesLabelId">
|
|
1623
|
+
<span class="field-label" [id]="variablesLabelId">{{ variablesLabel() }}</span>
|
|
1604
1624
|
@if (variables().length > 0) {
|
|
1605
1625
|
<div class="variable-list" role="group" [attr.aria-labelledby]="variablesLabelId">
|
|
1606
1626
|
@for (variable of variables(); track variable.name) {
|
|
1607
1627
|
<button kendoButton fillMode="flat" size="small" type="button"
|
|
1608
|
-
[title]="'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')'"
|
|
1628
|
+
[title]="showPreview() ? 'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')' : 'Insert ' + variable.name"
|
|
1609
1629
|
(click)="insertVariable(variable.name)">
|
|
1610
1630
|
{{ variable.name }}@if (variable.source === 'widget') { <span class="variable-source">(widget)</span> }
|
|
1611
1631
|
</button>
|
|
1612
1632
|
}
|
|
1613
1633
|
</div>
|
|
1614
1634
|
} @else {
|
|
1615
|
-
<p class="field-hint">
|
|
1635
|
+
<p class="field-hint">{{ emptyVariablesText() }}</p>
|
|
1616
1636
|
}
|
|
1617
1637
|
</div>
|
|
1618
1638
|
`, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:20px}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label,.field-label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface)}.required,.field-error{color:var(--kendo-color-error)}.field-hint,.field-error{margin:0;font-size:.8rem}.field-hint,.variable-source{color:var(--kendo-color-subtle)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: FormulaValidityDirective, selector: "[mmFormulaValidity]", inputs: ["mmFormulaValidity"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
@@ -1631,7 +1651,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1631
1651
|
[inputAttributes]="inputAttributes()">
|
|
1632
1652
|
</kendo-textbox>
|
|
1633
1653
|
<p class="field-hint" [id]="hintId">
|
|
1634
|
-
Reference
|
|
1654
|
+
Reference {{ namesNoun() }} with <code>{{ syntaxHint() }}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
|
|
1635
1655
|
<code>min</code>, <code>max</code>, <code>abs</code>, <code>round</code>, <code>clamp</code>, <code>? :</code>.
|
|
1636
1656
|
Numbers use a dot as decimal separator.
|
|
1637
1657
|
@if (clampHint()) {
|
|
@@ -1640,28 +1660,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
1640
1660
|
</p>
|
|
1641
1661
|
@if (formula().trim() && validation().error) {
|
|
1642
1662
|
<p class="field-error" [id]="messageId" role="alert">{{ validation().error }}</p>
|
|
1643
|
-
} @else if (formula().trim()) {
|
|
1663
|
+
} @else if (formula().trim() && showPreview()) {
|
|
1644
1664
|
<p class="field-hint" [id]="messageId" aria-live="polite">Preview: <strong>{{ preview() }}</strong></p>
|
|
1645
1665
|
}
|
|
1646
1666
|
</div>
|
|
1647
1667
|
<div class="form-field">
|
|
1648
|
-
<span class="field-label" [id]="variablesLabelId">
|
|
1668
|
+
<span class="field-label" [id]="variablesLabelId">{{ variablesLabel() }}</span>
|
|
1649
1669
|
@if (variables().length > 0) {
|
|
1650
1670
|
<div class="variable-list" role="group" [attr.aria-labelledby]="variablesLabelId">
|
|
1651
1671
|
@for (variable of variables(); track variable.name) {
|
|
1652
1672
|
<button kendoButton fillMode="flat" size="small" type="button"
|
|
1653
|
-
[title]="'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')'"
|
|
1673
|
+
[title]="showPreview() ? 'Insert ' + variable.name + ' (current value: ' + (variable.value || '-') + ')' : 'Insert ' + variable.name"
|
|
1654
1674
|
(click)="insertVariable(variable.name)">
|
|
1655
1675
|
{{ variable.name }}@if (variable.source === 'widget') { <span class="variable-source">(widget)</span> }
|
|
1656
1676
|
</button>
|
|
1657
1677
|
}
|
|
1658
1678
|
</div>
|
|
1659
1679
|
} @else {
|
|
1660
|
-
<p class="field-hint">
|
|
1680
|
+
<p class="field-hint">{{ emptyVariablesText() }}</p>
|
|
1661
1681
|
}
|
|
1662
1682
|
</div>
|
|
1663
1683
|
`, styles: [":host{display:flex;flex-direction:column;gap:20px}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label,.field-label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface)}.required,.field-error{color:var(--kendo-color-error)}.field-hint,.field-error{margin:0;font-size:.8rem}.field-hint,.variable-source{color:var(--kendo-color-subtle)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}\n"] }]
|
|
1664
|
-
}], propDecorators: { formula: [{ type: i0.Input, args: [{ isSignal: true, alias: "formula", required: false }] }, { type: i0.Output, args: ["formulaChange"] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], declaredNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "declaredNames", required: false }] }], numericOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "numericOnly", required: false }] }], clampHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "clampHint", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
|
|
1684
|
+
}], propDecorators: { formula: [{ type: i0.Input, args: [{ isSignal: true, alias: "formula", required: false }] }, { type: i0.Output, args: ["formulaChange"] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], declaredNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "declaredNames", required: false }] }], numericOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "numericOnly", required: false }] }], clampHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "clampHint", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], variablesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "variablesLabel", required: false }] }], namesNoun: [{ type: i0.Input, args: [{ isSignal: true, alias: "namesNoun", required: false }] }], syntaxHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "syntaxHint", required: false }] }], emptyVariablesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyVariablesText", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
|
|
1665
1685
|
|
|
1666
1686
|
const GetDashboardsDocumentDto = gql `
|
|
1667
1687
|
query getDashboards($first: Int) {
|
|
@@ -4332,7 +4352,8 @@ class QueryExecutorService {
|
|
|
4332
4352
|
fieldFilter: options.fieldFilter ?? undefined,
|
|
4333
4353
|
arg
|
|
4334
4354
|
},
|
|
4335
|
-
fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
|
|
4355
|
+
fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first',
|
|
4356
|
+
...(options.silentErrors ? { context: { [OCTO_SILENT_ERRORS]: true } } : {})
|
|
4336
4357
|
}).pipe(map$1(result => {
|
|
4337
4358
|
const queryItem = result.data?.streamData?.streamDataQuery?.items?.[0];
|
|
4338
4359
|
if (!queryItem) {
|
|
@@ -4355,23 +4376,43 @@ class QueryExecutorService {
|
|
|
4355
4376
|
* Runs the query with `first: 1` and its own window first; a query that cannot run
|
|
4356
4377
|
* without a window — an `Integral` column over a raw archive requires from/to
|
|
4357
4378
|
* (AB#7921) — fails or returns no columns, so it is run once more with `window`
|
|
4358
|
-
* (the board's time window or a recent default). The error of
|
|
4379
|
+
* (the board's time window or a recent default, see `columnProbeWindow`). The error of
|
|
4380
|
+
* the retry propagates. `options.forceRefresh` bypasses the Apollo cache for both calls.
|
|
4381
|
+
*
|
|
4382
|
+
* The first, window-less attempt is a probe: its error is expected for such queries
|
|
4383
|
+
* and is not shown as an error toast (AB#7985). Only the retry's error reaches the
|
|
4384
|
+
* global error handler.
|
|
4385
|
+
*/
|
|
4386
|
+
async fetchStreamDataColumns(queryRtId, window, options = {}) {
|
|
4387
|
+
const result = await this.executeWithProbeWindow('streamData', queryRtId, { first: 1, forceRefresh: options.forceRefresh ?? false }, window);
|
|
4388
|
+
return result.columns;
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* Runs a persisted query for a config picker (column list, category values, AB#7975).
|
|
4392
|
+
* Runtime queries run once as {@link execute}. A stream-data query runs with its own
|
|
4393
|
+
* window first — silently, its refusal is expected for an Integral query without from/to
|
|
4394
|
+
* (AB#7985) — and, when that fails or returns no columns, once more with `window`
|
|
4395
|
+
* (`columnProbeWindow`). The error of the retry propagates and is shown.
|
|
4359
4396
|
*/
|
|
4360
|
-
async
|
|
4397
|
+
async executeWithProbeWindow(family, queryRtId, options, window) {
|
|
4398
|
+
const resolved = family ?? await this.resolveFamily(queryRtId);
|
|
4399
|
+
if (resolved !== 'streamData') {
|
|
4400
|
+
return firstValueFrom(this.executeRuntime(queryRtId, options));
|
|
4401
|
+
}
|
|
4361
4402
|
try {
|
|
4362
|
-
const result = await firstValueFrom(this.executeStreamData(queryRtId, {
|
|
4403
|
+
const result = await firstValueFrom(this.executeStreamData(queryRtId, { ...options, silentErrors: true }));
|
|
4363
4404
|
if (result.columns.length > 0) {
|
|
4364
|
-
return result
|
|
4405
|
+
return result;
|
|
4365
4406
|
}
|
|
4366
4407
|
}
|
|
4367
4408
|
catch {
|
|
4368
4409
|
// Fall through to the windowed retry below.
|
|
4369
4410
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4411
|
+
return firstValueFrom(this.executeStreamData(queryRtId, {
|
|
4412
|
+
...options,
|
|
4413
|
+
silentErrors: false,
|
|
4414
|
+
streamDataArgs: { ...options.streamDataArgs, from: window.from, to: window.to }
|
|
4373
4415
|
}));
|
|
4374
|
-
return windowed.columns;
|
|
4375
4416
|
}
|
|
4376
4417
|
/**
|
|
4377
4418
|
* Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
|
|
@@ -7546,6 +7587,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
7546
7587
|
type: Output
|
|
7547
7588
|
}] } });
|
|
7548
7589
|
|
|
7590
|
+
/**
|
|
7591
|
+
* Time window the widget config dialogs use to list the columns of a persisted
|
|
7592
|
+
* stream-data query (AB#7921, AB#7975).
|
|
7593
|
+
*
|
|
7594
|
+
* A query with an `Integral` (or TimeWeightedAverage / StateDuration) column over a
|
|
7595
|
+
* raw archive only runs with from/to, so {@link QueryExecutorService.fetchStreamDataColumns}
|
|
7596
|
+
* retries with this window: the board's time window when the board has a time filter,
|
|
7597
|
+
* otherwise the last 24 hours. The window only serves to surface the column metadata;
|
|
7598
|
+
* the widget itself always runs with the board window.
|
|
7599
|
+
*/
|
|
7600
|
+
const COLUMN_PROBE_FALLBACK_MS = 24 * 60 * 60 * 1000;
|
|
7601
|
+
function columnProbeWindow(boardWindow, now = new Date()) {
|
|
7602
|
+
if (boardWindow) {
|
|
7603
|
+
return boardWindow;
|
|
7604
|
+
}
|
|
7605
|
+
return { from: new Date(now.getTime() - COLUMN_PROBE_FALLBACK_MS), to: now };
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7549
7608
|
/**
|
|
7550
7609
|
* Configuration dialog for KPI widgets.
|
|
7551
7610
|
* Allows selecting data source, value attribute, and display options.
|
|
@@ -8116,21 +8175,13 @@ class KpiConfigDialogComponent {
|
|
|
8116
8175
|
// Stream-data: execute with a tiny page just to surface columns. An Integral
|
|
8117
8176
|
// column needs from/to (AB#7921), so the executor retries with the board's
|
|
8118
8177
|
// window — or the last 24 hours when the board has no time filter.
|
|
8119
|
-
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, this.
|
|
8178
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
8120
8179
|
return columns.map(c => ({
|
|
8121
8180
|
attributePath: c.attributePath,
|
|
8122
8181
|
attributeValueType: c.attributeValueType ?? '',
|
|
8123
8182
|
aggregationType: c.aggregationType ?? null
|
|
8124
8183
|
}));
|
|
8125
8184
|
}
|
|
8126
|
-
columnProbeWindow() {
|
|
8127
|
-
const boardWindow = this.meshBoardStateService.resolveStreamDataTimeArgs();
|
|
8128
|
-
if (boardWindow) {
|
|
8129
|
-
return boardWindow;
|
|
8130
|
-
}
|
|
8131
|
-
const to = new Date();
|
|
8132
|
-
return { from: new Date(to.getTime() - 24 * 60 * 60 * 1000), to };
|
|
8133
|
-
}
|
|
8134
8185
|
async onCategoryFieldChange(categoryField) {
|
|
8135
8186
|
this.form.queryCategoryField = categoryField;
|
|
8136
8187
|
this.form.queryCategoryValue = '';
|
|
@@ -8144,7 +8195,8 @@ class KpiConfigDialogComponent {
|
|
|
8144
8195
|
try {
|
|
8145
8196
|
// family may be undefined here — the executor falls back to a lookup.
|
|
8146
8197
|
const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
|
|
8147
|
-
|
|
8198
|
+
// A grouped Integral query needs from/to like its column list (AB#7975).
|
|
8199
|
+
const result = await this.queryExecutor.executeWithProbeWindow(family, queryRtId, { first: 100 }, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
8148
8200
|
const values = new Set();
|
|
8149
8201
|
for (const row of result.rows) {
|
|
8150
8202
|
if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
|
|
@@ -8602,7 +8654,7 @@ class KpiConfigDialogComponent {
|
|
|
8602
8654
|
</button>
|
|
8603
8655
|
</div>
|
|
8604
8656
|
</div>
|
|
8605
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
8657
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
8606
8658
|
}
|
|
8607
8659
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
|
|
8608
8660
|
type: Component,
|
|
@@ -11872,7 +11924,8 @@ class TableConfigDialogComponent {
|
|
|
11872
11924
|
/**
|
|
11873
11925
|
* The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
|
|
11874
11926
|
* queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
|
|
11875
|
-
* (no column-introspection endpoint)
|
|
11927
|
+
* (no column-introspection endpoint) through `fetchStreamDataColumns`, which retries with a
|
|
11928
|
+
* time window for queries that need one (Integral, AB#7975), like the chart dialogs.
|
|
11876
11929
|
*/
|
|
11877
11930
|
loadQueryFields(rtId) {
|
|
11878
11931
|
if (this.queryFieldsLoad?.rtId === rtId) {
|
|
@@ -11893,8 +11946,10 @@ class TableConfigDialogComponent {
|
|
|
11893
11946
|
.map(c => c?.attributePath ?? '')
|
|
11894
11947
|
.filter(path => path !== '');
|
|
11895
11948
|
}
|
|
11896
|
-
|
|
11897
|
-
|
|
11949
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
11950
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
11951
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()), { forceRefresh: true });
|
|
11952
|
+
return columns.map(c => c.attributePath).filter(path => !!path);
|
|
11898
11953
|
}
|
|
11899
11954
|
catch (error) {
|
|
11900
11955
|
console.error('Error loading query columns:', error);
|
|
@@ -12729,9 +12784,16 @@ class GaugeWidgetComponent {
|
|
|
12729
12784
|
value = this.extractGroupedAggregationValue(result);
|
|
12730
12785
|
break;
|
|
12731
12786
|
}
|
|
12732
|
-
// Create a synthetic entity with the value
|
|
12733
12787
|
if (generation !== this.loadGeneration)
|
|
12734
12788
|
return;
|
|
12789
|
+
// A null cell is "no data" (e.g. an Integral over an empty window, AB#7975): the gauge
|
|
12790
|
+
// shows its empty state ("-", pointer at the scale start), not a misleading 0.
|
|
12791
|
+
if (value === null) {
|
|
12792
|
+
this._data.set(null);
|
|
12793
|
+
this._isLoading.set(false);
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12796
|
+
// Create a synthetic entity with the value
|
|
12735
12797
|
const gaugeEntity = {
|
|
12736
12798
|
rtId: 'query-entity',
|
|
12737
12799
|
ckTypeId: 'system.query',
|
|
@@ -12796,7 +12858,10 @@ class GaugeWidgetComponent {
|
|
|
12796
12858
|
}
|
|
12797
12859
|
return 0;
|
|
12798
12860
|
}
|
|
12861
|
+
/** Parses a cell value; a null cell stays null ("no data", AB#7975), unparsable text is 0. */
|
|
12799
12862
|
parseNumericValue(value) {
|
|
12863
|
+
if (value === null || value === undefined)
|
|
12864
|
+
return null;
|
|
12800
12865
|
if (typeof value === 'number')
|
|
12801
12866
|
return value;
|
|
12802
12867
|
const parsed = parseFloat(String(value));
|
|
@@ -13660,8 +13725,10 @@ class GaugeConfigDialogComponent {
|
|
|
13660
13725
|
aggregationType: c.aggregationType ?? null
|
|
13661
13726
|
}));
|
|
13662
13727
|
}
|
|
13663
|
-
|
|
13664
|
-
|
|
13728
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
13729
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
13730
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
13731
|
+
return columns.map(c => ({
|
|
13665
13732
|
attributePath: c.attributePath,
|
|
13666
13733
|
attributeValueType: c.attributeValueType ?? '',
|
|
13667
13734
|
aggregationType: c.aggregationType ?? null
|
|
@@ -13680,7 +13747,8 @@ class GaugeConfigDialogComponent {
|
|
|
13680
13747
|
// family may be undefined — the executor falls back to a lookup.
|
|
13681
13748
|
const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
|
|
13682
13749
|
try {
|
|
13683
|
-
|
|
13750
|
+
// A grouped Integral query needs from/to like its column list (AB#7975).
|
|
13751
|
+
const result = await this.queryExecutor.executeWithProbeWindow(family, queryRtId, { first: 100 }, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
13684
13752
|
const values = new Set();
|
|
13685
13753
|
for (const row of result.rows) {
|
|
13686
13754
|
if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
|
|
@@ -14144,7 +14212,7 @@ class GaugeConfigDialogComponent {
|
|
|
14144
14212
|
</button>
|
|
14145
14213
|
</div>
|
|
14146
14214
|
</div>
|
|
14147
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
14215
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
14148
14216
|
}
|
|
14149
14217
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
|
|
14150
14218
|
type: Component,
|
|
@@ -14968,14 +15036,18 @@ class PieChartWidgetComponent {
|
|
|
14968
15036
|
const categoryCell = findCellForField(row.cells, this.config.categoryField);
|
|
14969
15037
|
const valueCell = findCellForField(row.cells, this.config.valueField);
|
|
14970
15038
|
const category = categoryCell ? String(categoryCell.value ?? '') : '';
|
|
15039
|
+
// A null cell is "no data" (e.g. an Integral over an empty window, AB#7975):
|
|
15040
|
+
// the slice is left out instead of being shown as 0.
|
|
15041
|
+
const noData = valueCell !== undefined && (valueCell.value === null || valueCell.value === undefined);
|
|
14971
15042
|
let value = 0;
|
|
14972
|
-
if (valueCell) {
|
|
15043
|
+
if (valueCell && !noData) {
|
|
14973
15044
|
const numValue = typeof valueCell.value === 'number' ? valueCell.value : parseFloat(String(valueCell.value));
|
|
14974
15045
|
value = isNaN(numValue) ? 0 : numValue;
|
|
14975
15046
|
}
|
|
14976
|
-
return { category, value };
|
|
15047
|
+
return { category, value, noData };
|
|
14977
15048
|
})
|
|
14978
|
-
.filter(item => item.category !== '')
|
|
15049
|
+
.filter(item => item.category !== '' && !item.noData) // Filter out empty categories and no-data slices
|
|
15050
|
+
.map(({ category, value }) => ({ category, value }));
|
|
14979
15051
|
this._rawData.set(chartData);
|
|
14980
15052
|
this._isLoading.set(false);
|
|
14981
15053
|
}
|
|
@@ -15370,8 +15442,10 @@ class PieChartConfigDialogComponent {
|
|
|
15370
15442
|
aggregationType: c.aggregationType ?? null
|
|
15371
15443
|
}));
|
|
15372
15444
|
}
|
|
15373
|
-
|
|
15374
|
-
|
|
15445
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
15446
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
15447
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
|
|
15448
|
+
return columns.map(c => ({
|
|
15375
15449
|
attributePath: c.attributePath,
|
|
15376
15450
|
attributeValueType: c.attributeValueType ?? '',
|
|
15377
15451
|
aggregationType: c.aggregationType ?? null
|
|
@@ -16011,6 +16085,541 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
16011
16085
|
type: Input
|
|
16012
16086
|
}] } });
|
|
16013
16087
|
|
|
16088
|
+
/** Referenced series keys of a formula, distinct, in order of first appearance. */
|
|
16089
|
+
function seriesFormulaOperands(formula) {
|
|
16090
|
+
return rewriteFormula(formula ?? '').referencedNames;
|
|
16091
|
+
}
|
|
16092
|
+
/**
|
|
16093
|
+
* Orders formula series so every series comes after the formula series it references (Kahn's
|
|
16094
|
+
* algorithm, stable in input order). References to non-formula keys (query series) are leaves.
|
|
16095
|
+
* Series in or behind a cycle are left out of `order`; `cyclic` names the ones in the cycle.
|
|
16096
|
+
*/
|
|
16097
|
+
function orderSeriesFormulas(formulas) {
|
|
16098
|
+
const keys = new Set(formulas.map(f => f.key));
|
|
16099
|
+
const deps = new Map();
|
|
16100
|
+
for (const f of formulas) {
|
|
16101
|
+
deps.set(f.key, seriesFormulaOperands(f.formula).filter(k => keys.has(k)));
|
|
16102
|
+
}
|
|
16103
|
+
const order = [];
|
|
16104
|
+
const done = new Set();
|
|
16105
|
+
let progress = true;
|
|
16106
|
+
while (progress) {
|
|
16107
|
+
progress = false;
|
|
16108
|
+
for (const f of formulas) {
|
|
16109
|
+
if (done.has(f.key))
|
|
16110
|
+
continue;
|
|
16111
|
+
if ((deps.get(f.key) ?? []).every(d => done.has(d))) {
|
|
16112
|
+
done.add(f.key);
|
|
16113
|
+
order.push(f.key);
|
|
16114
|
+
progress = true;
|
|
16115
|
+
}
|
|
16116
|
+
}
|
|
16117
|
+
}
|
|
16118
|
+
const cyclic = formulas
|
|
16119
|
+
.filter(f => !done.has(f.key) && reaches(f.key, f.key, deps))
|
|
16120
|
+
.map(f => f.key);
|
|
16121
|
+
return { order, cyclic };
|
|
16122
|
+
}
|
|
16123
|
+
/** True when `target` is reachable from the dependencies of `start`. */
|
|
16124
|
+
function reaches(start, target, deps) {
|
|
16125
|
+
const visited = new Set();
|
|
16126
|
+
const stack = [...(deps.get(start) ?? [])];
|
|
16127
|
+
while (stack.length > 0) {
|
|
16128
|
+
const current = stack.pop();
|
|
16129
|
+
if (current === target)
|
|
16130
|
+
return true;
|
|
16131
|
+
if (visited.has(current))
|
|
16132
|
+
continue;
|
|
16133
|
+
visited.add(current);
|
|
16134
|
+
stack.push(...(deps.get(current) ?? []));
|
|
16135
|
+
}
|
|
16136
|
+
return false;
|
|
16137
|
+
}
|
|
16138
|
+
/** Message of a circular reference, naming the series involved. */
|
|
16139
|
+
function circularReferenceMessage(cyclic) {
|
|
16140
|
+
return `Circular reference between series: ${cyclic.join(', ')}`;
|
|
16141
|
+
}
|
|
16142
|
+
/**
|
|
16143
|
+
* Validates every formula series of a chart: syntax and unknown keys exactly like the KPI formula
|
|
16144
|
+
* field (`validateFormula`, series keys as available names), then circular references.
|
|
16145
|
+
* Returns one result per formula, in input order.
|
|
16146
|
+
*/
|
|
16147
|
+
function validateSeriesFormulas(formulas, availableKeys, evaluator) {
|
|
16148
|
+
const results = formulas.map(f => validateFormula(f.formula, availableKeys, evaluator));
|
|
16149
|
+
const { cyclic } = orderSeriesFormulas(formulas);
|
|
16150
|
+
if (cyclic.length > 0) {
|
|
16151
|
+
const message = circularReferenceMessage(cyclic);
|
|
16152
|
+
formulas.forEach((f, i) => {
|
|
16153
|
+
if (results[i].valid && cyclic.includes(f.key)) {
|
|
16154
|
+
results[i] = { valid: false, error: message, unknownVariables: [] };
|
|
16155
|
+
}
|
|
16156
|
+
});
|
|
16157
|
+
}
|
|
16158
|
+
return results;
|
|
16159
|
+
}
|
|
16160
|
+
/**
|
|
16161
|
+
* Evaluates the formula series bucket by bucket over `operands` (key → values, all of `length`).
|
|
16162
|
+
* Formula series are evaluated in dependency order and their results become operands of later
|
|
16163
|
+
* ones. A formula in a cycle, or with an operand key that has no values, is all gaps.
|
|
16164
|
+
* Returns key → values for every formula series.
|
|
16165
|
+
*/
|
|
16166
|
+
function evaluateSeriesFormulas(formulas, operands, length, evaluator) {
|
|
16167
|
+
const values = new Map(operands);
|
|
16168
|
+
const result = new Map();
|
|
16169
|
+
const byKey = new Map(formulas.map(f => [f.key, f]));
|
|
16170
|
+
const { order } = orderSeriesFormulas(formulas);
|
|
16171
|
+
for (const key of order) {
|
|
16172
|
+
const formula = byKey.get(key);
|
|
16173
|
+
const data = evaluateOneSeries(formula.formula, values, length, evaluator);
|
|
16174
|
+
result.set(key, data);
|
|
16175
|
+
values.set(key, data);
|
|
16176
|
+
}
|
|
16177
|
+
for (const f of formulas) {
|
|
16178
|
+
if (!result.has(f.key))
|
|
16179
|
+
result.set(f.key, new Array(length).fill(null));
|
|
16180
|
+
}
|
|
16181
|
+
return result;
|
|
16182
|
+
}
|
|
16183
|
+
function evaluateOneSeries(formula, values, length, evaluator) {
|
|
16184
|
+
const out = new Array(length).fill(null);
|
|
16185
|
+
if (!formula?.trim())
|
|
16186
|
+
return out;
|
|
16187
|
+
const { expression, referencedNames } = rewriteFormula(formula);
|
|
16188
|
+
const columns = referencedNames.map(name => values.get(name));
|
|
16189
|
+
if (columns.some(c => c === undefined))
|
|
16190
|
+
return out;
|
|
16191
|
+
// Identifiers of the operands, as rewriteFormula produced them (`${a}` alone rewrites to the identifier of `a`).
|
|
16192
|
+
const identifiers = referencedNames.map(name => rewriteFormula(`\${${name}}`).expression);
|
|
16193
|
+
for (let i = 0; i < length; i++) {
|
|
16194
|
+
const context = {};
|
|
16195
|
+
let gap = false;
|
|
16196
|
+
for (let j = 0; j < columns.length; j++) {
|
|
16197
|
+
const v = columns[j][i];
|
|
16198
|
+
if (v === null || v === undefined || !Number.isFinite(v)) {
|
|
16199
|
+
gap = true;
|
|
16200
|
+
break;
|
|
16201
|
+
}
|
|
16202
|
+
context[identifiers[j]] = v;
|
|
16203
|
+
}
|
|
16204
|
+
if (gap)
|
|
16205
|
+
continue;
|
|
16206
|
+
const r = evaluator.evaluate(expression, context);
|
|
16207
|
+
if (r.success && typeof r.value === 'number' && Number.isFinite(r.value)) {
|
|
16208
|
+
out[i] = r.value;
|
|
16209
|
+
}
|
|
16210
|
+
}
|
|
16211
|
+
return out;
|
|
16212
|
+
}
|
|
16213
|
+
|
|
16214
|
+
/** Suffix of the legend name of a formula series without any valid bucket. */
|
|
16215
|
+
const NO_DATA_LEGEND_SUFFIX = ' (no data)';
|
|
16216
|
+
/** Source key of the primary data source; series keys cannot clash with it (they start with a letter or `_`, never `__primary`). */
|
|
16217
|
+
const PRIMARY_SOURCE_KEY = '__primary';
|
|
16218
|
+
/** Valid series key: letters, digits and `_`, starting with a letter or `_`. */
|
|
16219
|
+
const SERIES_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16220
|
+
/** A query series the widget can load: key, query and value field set. */
|
|
16221
|
+
function isCompleteQuerySeries(def) {
|
|
16222
|
+
return def.kind === 'query'
|
|
16223
|
+
&& SERIES_KEY_PATTERN.test(def.key ?? '')
|
|
16224
|
+
&& def.key !== PRIMARY_SOURCE_KEY
|
|
16225
|
+
&& !!def.dataSource?.queryRtId
|
|
16226
|
+
&& !!def.valueField;
|
|
16227
|
+
}
|
|
16228
|
+
/** A formula series the widget can evaluate: valid key and a non-empty formula (validity is checked by the dialog). */
|
|
16229
|
+
function isCompleteFormulaSeries(def) {
|
|
16230
|
+
return def.kind === 'formula'
|
|
16231
|
+
&& SERIES_KEY_PATTERN.test(def.key ?? '')
|
|
16232
|
+
&& def.key !== PRIMARY_SOURCE_KEY
|
|
16233
|
+
&& !!def.formula?.trim();
|
|
16234
|
+
}
|
|
16235
|
+
/** Display name of a series: its label, else its key. */
|
|
16236
|
+
function querySeriesLabel(def) {
|
|
16237
|
+
return def.label?.trim() || def.key;
|
|
16238
|
+
}
|
|
16239
|
+
function toNumberOrNull(value) {
|
|
16240
|
+
if (value === null || value === undefined)
|
|
16241
|
+
return null;
|
|
16242
|
+
const n = typeof value === 'number' ? value : parseFloat(String(value));
|
|
16243
|
+
return isNaN(n) ? null : n;
|
|
16244
|
+
}
|
|
16245
|
+
/** Bucket start instant of a row: the downsampling bin / stream-data row timestamp, else the given category cell. */
|
|
16246
|
+
function rowInstant(row, categoryField, useTimestamp) {
|
|
16247
|
+
let raw = useTimestamp ? row.timestamp : undefined;
|
|
16248
|
+
if ((raw === undefined || raw === null) && categoryField) {
|
|
16249
|
+
raw = findCellForField(row.cells, categoryField)?.value;
|
|
16250
|
+
}
|
|
16251
|
+
const instant = toInstant$1(raw);
|
|
16252
|
+
return instant ? instant.getTime() : null;
|
|
16253
|
+
}
|
|
16254
|
+
/**
|
|
16255
|
+
* Value of a row for `valueField`. Downsampled rows carry `<field>_avg` / `_min` / `_max`: the
|
|
16256
|
+
* average (or an unsuffixed cell) is the value. Raw rows: the exact cell first (AB#4293 rule).
|
|
16257
|
+
* A null cell is "no data" (null), never 0.
|
|
16258
|
+
*/
|
|
16259
|
+
function rowValue(row, valueField, downsampled) {
|
|
16260
|
+
if (downsampled) {
|
|
16261
|
+
const cell = row.cells.find(c => {
|
|
16262
|
+
const lower = c.attributePath.toLowerCase();
|
|
16263
|
+
return lower !== 'timestamp' && !lower.endsWith('_min') && !lower.endsWith('_max')
|
|
16264
|
+
&& matchesAttributePath(c.attributePath, valueField);
|
|
16265
|
+
});
|
|
16266
|
+
return toNumberOrNull(cell?.value);
|
|
16267
|
+
}
|
|
16268
|
+
return toNumberOrNull(findCellForField(row.cells, valueField)?.value);
|
|
16269
|
+
}
|
|
16270
|
+
/**
|
|
16271
|
+
* Rows of the primary source → one series per `seriesGroupField` value, exactly the grouping of
|
|
16272
|
+
* the single-query chart (a later row of the same series and bucket wins).
|
|
16273
|
+
*/
|
|
16274
|
+
function primaryRowsToSeries(rows, fields, downsampled) {
|
|
16275
|
+
const series = new Map();
|
|
16276
|
+
for (const row of rows) {
|
|
16277
|
+
const instant = rowInstant(row, downsampled ? undefined : fields.categoryField, downsampled);
|
|
16278
|
+
if (instant === null)
|
|
16279
|
+
continue;
|
|
16280
|
+
const group = findCellForField(row.cells, fields.seriesGroupField)?.value;
|
|
16281
|
+
if (group === null || group === undefined || String(group) === '')
|
|
16282
|
+
continue;
|
|
16283
|
+
const name = String(group);
|
|
16284
|
+
let entry = series.get(name);
|
|
16285
|
+
if (!entry) {
|
|
16286
|
+
entry = { points: new Map() };
|
|
16287
|
+
series.set(name, entry);
|
|
16288
|
+
}
|
|
16289
|
+
entry.points.set(instant, rowValue(row, fields.valueField, downsampled));
|
|
16290
|
+
if (fields.unitField) {
|
|
16291
|
+
const unit = findCellForField(row.cells, fields.unitField)?.value;
|
|
16292
|
+
if (unit !== null && unit !== undefined && String(unit) !== '')
|
|
16293
|
+
entry.unit = String(unit);
|
|
16294
|
+
}
|
|
16295
|
+
}
|
|
16296
|
+
return Array.from(series.entries()).map(([name, e]) => ({ name, points: e.points, unit: e.unit }));
|
|
16297
|
+
}
|
|
16298
|
+
/**
|
|
16299
|
+
* Rows of a query series → its one series. Several rows in one bucket (e.g. one per source entity
|
|
16300
|
+
* when the scope holds several) are summed; a bucket whose rows are all null stays null.
|
|
16301
|
+
*/
|
|
16302
|
+
function queryRowsToSeries(rows, name, valueField, downsampled) {
|
|
16303
|
+
const points = new Map();
|
|
16304
|
+
for (const row of rows) {
|
|
16305
|
+
const instant = rowInstant(row, 'timestamp', true);
|
|
16306
|
+
if (instant === null)
|
|
16307
|
+
continue;
|
|
16308
|
+
const value = rowValue(row, valueField, downsampled);
|
|
16309
|
+
const prev = points.get(instant);
|
|
16310
|
+
if (prev === undefined || prev === null)
|
|
16311
|
+
points.set(instant, value);
|
|
16312
|
+
else if (value !== null)
|
|
16313
|
+
points.set(instant, prev + value);
|
|
16314
|
+
}
|
|
16315
|
+
return { name, points };
|
|
16316
|
+
}
|
|
16317
|
+
/**
|
|
16318
|
+
* Category labels for aligned instants. A label that repeats (the doubled wall-clock hour of a
|
|
16319
|
+
* DST fall-back day) gets ` (2)`, ` (3)` …, so every bucket keeps its own category.
|
|
16320
|
+
*/
|
|
16321
|
+
function uniqueCategoryLabels(instants, format) {
|
|
16322
|
+
const seen = new Map();
|
|
16323
|
+
return instants.map(t => {
|
|
16324
|
+
const label = format(new Date(t));
|
|
16325
|
+
const n = (seen.get(label) ?? 0) + 1;
|
|
16326
|
+
seen.set(label, n);
|
|
16327
|
+
return n === 1 ? label : `${label} (${n})`;
|
|
16328
|
+
});
|
|
16329
|
+
}
|
|
16330
|
+
/**
|
|
16331
|
+
* Validation message of each series row of the config dialog (query and formula rows, AB#7951 /
|
|
16332
|
+
* AB#7952), `null` when the row is valid. Keys must match {@link SERIES_KEY_PATTERN} and be unique
|
|
16333
|
+
* across all rows; query rows need a query and a value field; formula rows are validated like the
|
|
16334
|
+
* KPI formula field (syntax, unknown keys — every series key is an available name) and must not be
|
|
16335
|
+
* part of a circular reference. With `requireCategoryField` (bar chart, AB#7953) query rows also
|
|
16336
|
+
* need a category field.
|
|
16337
|
+
*/
|
|
16338
|
+
function querySeriesErrors(series, evaluator, options) {
|
|
16339
|
+
const counts = new Map();
|
|
16340
|
+
for (const s of series) {
|
|
16341
|
+
const key = s.key?.trim() ?? '';
|
|
16342
|
+
if (key)
|
|
16343
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
16344
|
+
}
|
|
16345
|
+
const keys = [...counts.keys()];
|
|
16346
|
+
const formulaRows = series
|
|
16347
|
+
.map((s, index) => ({ s, index }))
|
|
16348
|
+
.filter((r) => r.s.kind === 'formula');
|
|
16349
|
+
const formulaResults = evaluator
|
|
16350
|
+
? validateSeriesFormulas(formulaRows.map(r => ({ key: r.s.key?.trim() ?? '', formula: r.s.formula ?? '' })), keys, evaluator)
|
|
16351
|
+
: [];
|
|
16352
|
+
const formulaErrorByIndex = new Map();
|
|
16353
|
+
formulaRows.forEach((r, i) => {
|
|
16354
|
+
const result = formulaResults[i];
|
|
16355
|
+
formulaErrorByIndex.set(r.index, result && !result.valid ? (result.error ?? 'Invalid formula') : null);
|
|
16356
|
+
});
|
|
16357
|
+
return series.map((s, index) => {
|
|
16358
|
+
const key = s.key?.trim() ?? '';
|
|
16359
|
+
if (!key)
|
|
16360
|
+
return 'Enter a key.';
|
|
16361
|
+
if (!SERIES_KEY_PATTERN.test(key) || key === PRIMARY_SOURCE_KEY) {
|
|
16362
|
+
return 'The key may contain letters, digits and _ and must not start with a digit.';
|
|
16363
|
+
}
|
|
16364
|
+
if ((counts.get(key) ?? 0) > 1)
|
|
16365
|
+
return `The key "${key}" is used by another series.`;
|
|
16366
|
+
if (s.kind === 'formula') {
|
|
16367
|
+
if (!s.formula?.trim())
|
|
16368
|
+
return 'Enter a formula.';
|
|
16369
|
+
return formulaErrorByIndex.get(index) ?? null;
|
|
16370
|
+
}
|
|
16371
|
+
if (!s.dataSource?.queryRtId)
|
|
16372
|
+
return 'Select a stream-data query.';
|
|
16373
|
+
if (options?.requireCategoryField && !s.categoryField)
|
|
16374
|
+
return 'Select a category field.';
|
|
16375
|
+
if (!s.valueField)
|
|
16376
|
+
return 'Select a value field.';
|
|
16377
|
+
if (s.dataSource.resolutionAware && !s.dataSource.requiredAggregation)
|
|
16378
|
+
return 'Select the aggregation.';
|
|
16379
|
+
return null;
|
|
16380
|
+
});
|
|
16381
|
+
}
|
|
16382
|
+
/**
|
|
16383
|
+
* Composes the series the chart draws from the aligned grid (AB#7952):
|
|
16384
|
+
*
|
|
16385
|
+
* 1. Operands = the aligned query series by key (hidden ones included — they went through the
|
|
16386
|
+
* alignment like every other series).
|
|
16387
|
+
* 2. Formula series are evaluated per bucket (`evaluateSeriesFormulas`: dependency order, null
|
|
16388
|
+
* operand or non-finite result = gap).
|
|
16389
|
+
* 3. Order: primary series, then `additionalSeries` in their order; hidden series (`visible:
|
|
16390
|
+
* false`, primary via `primary.visible`) are left out. In a stacked area chart a series with
|
|
16391
|
+
* `stacked: false` is drawn as a line and moved behind the stacked ones, so it lies over the stack.
|
|
16392
|
+
* 4. Hints: a visible formula series that (directly or through other formulas) uses a failed or
|
|
16393
|
+
* misaligned query source gets "depends on <source>, which failed"; a visible formula series without any
|
|
16394
|
+
* valid bucket keeps its legend entry with the suffix " (no data)".
|
|
16395
|
+
*/
|
|
16396
|
+
function composeChartSeries(args) {
|
|
16397
|
+
const { gridSeries, length, definitions, primary, stackedChart, sourceProblems, evaluator } = args;
|
|
16398
|
+
const formulas = definitions.filter(isCompleteFormulaSeries);
|
|
16399
|
+
const operands = new Map();
|
|
16400
|
+
for (const s of gridSeries) {
|
|
16401
|
+
if (s.sourceKey !== PRIMARY_SOURCE_KEY)
|
|
16402
|
+
operands.set(s.sourceKey, s.data);
|
|
16403
|
+
}
|
|
16404
|
+
const evaluated = formulas.length > 0
|
|
16405
|
+
? evaluateSeriesFormulas(formulas, operands, length, evaluator)
|
|
16406
|
+
: new Map();
|
|
16407
|
+
const placement = (options) => {
|
|
16408
|
+
const stacked = stackedChart && options?.stacked !== false;
|
|
16409
|
+
return { stacked, asLine: stackedChart && !stacked };
|
|
16410
|
+
};
|
|
16411
|
+
const series = [];
|
|
16412
|
+
if (primary?.visible !== false) {
|
|
16413
|
+
for (const s of gridSeries) {
|
|
16414
|
+
if (s.sourceKey === PRIMARY_SOURCE_KEY)
|
|
16415
|
+
series.push({ name: s.name, data: s.data, unit: s.unit, ...placement(primary) });
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
const hints = [];
|
|
16419
|
+
const formulaByKey = new Map(formulas.map(f => [f.key, f]));
|
|
16420
|
+
for (const def of definitions) {
|
|
16421
|
+
if (def.kind === 'query') {
|
|
16422
|
+
if (def.visible === false || !isCompleteQuerySeries(def))
|
|
16423
|
+
continue;
|
|
16424
|
+
const aligned = gridSeries.find(s => s.sourceKey === def.key);
|
|
16425
|
+
if (aligned)
|
|
16426
|
+
series.push({ name: aligned.name, data: aligned.data, unit: aligned.unit, ...placement(def) });
|
|
16427
|
+
continue;
|
|
16428
|
+
}
|
|
16429
|
+
if (!isCompleteFormulaSeries(def) || def.visible === false)
|
|
16430
|
+
continue;
|
|
16431
|
+
const label = querySeriesLabel(def);
|
|
16432
|
+
const failed = failedSourcesOf(def.key, formulaByKey, sourceProblems);
|
|
16433
|
+
for (const source of failed) {
|
|
16434
|
+
hints.push({
|
|
16435
|
+
kind: 'dependency',
|
|
16436
|
+
sourceLabel: label,
|
|
16437
|
+
text: `${label}: depends on ${source.label}, which ${source.problem === 'misaligned' ? 'is not on the chart\'s time grid' : 'failed'}`
|
|
16438
|
+
});
|
|
16439
|
+
}
|
|
16440
|
+
const data = evaluated.get(def.key) ?? new Array(length).fill(null);
|
|
16441
|
+
const hasData = data.some(v => v !== null);
|
|
16442
|
+
series.push({ name: hasData ? label : `${label}${NO_DATA_LEGEND_SUFFIX}`, data, ...placement(def) });
|
|
16443
|
+
}
|
|
16444
|
+
// Lines over the stack come last, so Kendo draws them on top.
|
|
16445
|
+
return { series: [...series.filter(s => !s.asLine), ...series.filter(s => s.asLine)], hints };
|
|
16446
|
+
}
|
|
16447
|
+
/** Failed or misaligned query sources a formula series depends on, directly or through other formulas. */
|
|
16448
|
+
function failedSourcesOf(key, formulaByKey, sourceProblems) {
|
|
16449
|
+
const out = [];
|
|
16450
|
+
const visited = new Set();
|
|
16451
|
+
const stack = [key];
|
|
16452
|
+
while (stack.length > 0) {
|
|
16453
|
+
const current = stack.pop();
|
|
16454
|
+
if (visited.has(current))
|
|
16455
|
+
continue;
|
|
16456
|
+
visited.add(current);
|
|
16457
|
+
const formula = formulaByKey.get(current);
|
|
16458
|
+
if (formula) {
|
|
16459
|
+
stack.push(...seriesFormulaOperands(formula.formula).reverse());
|
|
16460
|
+
continue;
|
|
16461
|
+
}
|
|
16462
|
+
const problem = sourceProblems.get(current);
|
|
16463
|
+
if (problem && problem.problem !== 'noData')
|
|
16464
|
+
out.push(problem);
|
|
16465
|
+
}
|
|
16466
|
+
return out;
|
|
16467
|
+
}
|
|
16468
|
+
|
|
16469
|
+
/** Category of a raw cell value; `null` for an empty value (the row is skipped, as today). */
|
|
16470
|
+
function categoryOf(raw) {
|
|
16471
|
+
if (raw === null || raw === undefined || String(raw) === '')
|
|
16472
|
+
return null;
|
|
16473
|
+
if (raw instanceof Date || isIsoDateTime(raw)) {
|
|
16474
|
+
const instant = toInstant$1(raw);
|
|
16475
|
+
if (instant)
|
|
16476
|
+
return { key: `t:${instant.getTime()}`, instant: instant.getTime(), raw };
|
|
16477
|
+
}
|
|
16478
|
+
return { key: `s:${String(raw)}`, instant: null, raw };
|
|
16479
|
+
}
|
|
16480
|
+
/**
|
|
16481
|
+
* Raw category value of a row: the cell of `categoryField` (exact match first, AB#4293). A
|
|
16482
|
+
* stream-data row carries its time stamp as row property, not as cell, so `timestamp` falls back
|
|
16483
|
+
* to `row.timestamp`.
|
|
16484
|
+
*/
|
|
16485
|
+
function rowCategory(row, categoryField) {
|
|
16486
|
+
let raw = findCellForField(row.cells, categoryField)?.value;
|
|
16487
|
+
if ((raw === undefined || raw === null) && categoryField.toLowerCase() === 'timestamp')
|
|
16488
|
+
raw = row.timestamp;
|
|
16489
|
+
return categoryOf(raw);
|
|
16490
|
+
}
|
|
16491
|
+
/**
|
|
16492
|
+
* Bar value of a cell, as the single-query chart reads it: a number, `null` for a null cell ("no
|
|
16493
|
+
* data", AB#7975) and 0 for text that is not a number.
|
|
16494
|
+
*/
|
|
16495
|
+
function toBarValue(value) {
|
|
16496
|
+
if (value === null || value === undefined)
|
|
16497
|
+
return null;
|
|
16498
|
+
const n = typeof value === 'number' ? value : parseFloat(String(value));
|
|
16499
|
+
return isNaN(n) ? 0 : n;
|
|
16500
|
+
}
|
|
16501
|
+
function scaled(value, valueScale) {
|
|
16502
|
+
return value === null ? null : value * valueScale;
|
|
16503
|
+
}
|
|
16504
|
+
function addCategory(list, seen, category) {
|
|
16505
|
+
if (seen.has(category.key))
|
|
16506
|
+
return;
|
|
16507
|
+
seen.add(category.key);
|
|
16508
|
+
list.push(category);
|
|
16509
|
+
}
|
|
16510
|
+
/**
|
|
16511
|
+
* Rows of the primary source → its series, exactly the grouping of the single-query chart:
|
|
16512
|
+
* static mode one series per configured value field (a missing cell is 0), dynamic mode one series
|
|
16513
|
+
* per `seriesGroupField` value (a combination missing in a category of this source is 0). A later
|
|
16514
|
+
* row of the same category wins. `valueScale` is applied to every value.
|
|
16515
|
+
*/
|
|
16516
|
+
function primaryRowsToCategorySeries(rows, config, valueScale) {
|
|
16517
|
+
const categories = [];
|
|
16518
|
+
const seen = new Set();
|
|
16519
|
+
if (config.seriesGroupField && config.valueField) {
|
|
16520
|
+
const groups = new Map();
|
|
16521
|
+
for (const row of rows) {
|
|
16522
|
+
const category = rowCategory(row, config.categoryField);
|
|
16523
|
+
const groupCell = findCellForField(row.cells, config.seriesGroupField);
|
|
16524
|
+
const group = groupCell ? String(groupCell.value ?? '') : '';
|
|
16525
|
+
if (!category || !group)
|
|
16526
|
+
continue;
|
|
16527
|
+
addCategory(categories, seen, category);
|
|
16528
|
+
const valueCell = findCellForField(row.cells, config.valueField);
|
|
16529
|
+
const value = valueCell ? toBarValue(valueCell.value) : 0;
|
|
16530
|
+
let values = groups.get(group);
|
|
16531
|
+
if (!values) {
|
|
16532
|
+
values = new Map();
|
|
16533
|
+
groups.set(group, values);
|
|
16534
|
+
}
|
|
16535
|
+
values.set(category.key, scaled(value, valueScale));
|
|
16536
|
+
}
|
|
16537
|
+
const series = Array.from(groups.entries()).map(([name, values]) => {
|
|
16538
|
+
for (const c of categories)
|
|
16539
|
+
if (!values.has(c.key))
|
|
16540
|
+
values.set(c.key, 0);
|
|
16541
|
+
return { name, values };
|
|
16542
|
+
});
|
|
16543
|
+
return { categories, series };
|
|
16544
|
+
}
|
|
16545
|
+
const fields = config.series ?? [];
|
|
16546
|
+
const series = fields.map(f => ({ name: f.name ?? f.field, values: new Map() }));
|
|
16547
|
+
for (const row of rows) {
|
|
16548
|
+
const category = rowCategory(row, config.categoryField);
|
|
16549
|
+
if (!category)
|
|
16550
|
+
continue;
|
|
16551
|
+
addCategory(categories, seen, category);
|
|
16552
|
+
fields.forEach((f, i) => {
|
|
16553
|
+
const cell = findCellForField(row.cells, f.field);
|
|
16554
|
+
series[i].values.set(category.key, cell ? scaled(toBarValue(cell.value), valueScale) : 0);
|
|
16555
|
+
});
|
|
16556
|
+
}
|
|
16557
|
+
return { categories, series };
|
|
16558
|
+
}
|
|
16559
|
+
/**
|
|
16560
|
+
* Rows of a query series → its one series. Several rows in one category (e.g. one per source
|
|
16561
|
+
* entity when the scope holds several) are summed; a category whose rows are all null stays null.
|
|
16562
|
+
* `valueScale` is applied to the result.
|
|
16563
|
+
*/
|
|
16564
|
+
function queryRowsToCategorySeries(rows, name, categoryField, valueField, valueScale) {
|
|
16565
|
+
const categories = [];
|
|
16566
|
+
const seen = new Set();
|
|
16567
|
+
const values = new Map();
|
|
16568
|
+
for (const row of rows) {
|
|
16569
|
+
const category = rowCategory(row, categoryField);
|
|
16570
|
+
if (!category)
|
|
16571
|
+
continue;
|
|
16572
|
+
addCategory(categories, seen, category);
|
|
16573
|
+
const cell = findCellForField(row.cells, valueField);
|
|
16574
|
+
const value = cell ? toBarValue(cell.value) : null;
|
|
16575
|
+
const prev = values.get(category.key);
|
|
16576
|
+
if (prev === undefined || prev === null)
|
|
16577
|
+
values.set(category.key, value);
|
|
16578
|
+
else if (value !== null)
|
|
16579
|
+
values.set(category.key, prev + value);
|
|
16580
|
+
}
|
|
16581
|
+
for (const [key, v] of values)
|
|
16582
|
+
values.set(key, scaled(v, valueScale));
|
|
16583
|
+
return { categories, series: { name, values } };
|
|
16584
|
+
}
|
|
16585
|
+
/**
|
|
16586
|
+
* Matches the series of all sources per category value. Categories are the union over all sources:
|
|
16587
|
+
* in order of first appearance (primary source first, then the others in their order), and sorted
|
|
16588
|
+
* chronologically when every category is a date-time. A category a source lacks is `null` for
|
|
16589
|
+
* its series — no bar, never 0.
|
|
16590
|
+
*/
|
|
16591
|
+
function alignOnCategories(sources) {
|
|
16592
|
+
const categories = [];
|
|
16593
|
+
const seen = new Set();
|
|
16594
|
+
for (const source of sources) {
|
|
16595
|
+
for (const c of source.categories)
|
|
16596
|
+
addCategory(categories, seen, c);
|
|
16597
|
+
}
|
|
16598
|
+
if (categories.length > 0 && categories.every(c => c.instant !== null)) {
|
|
16599
|
+
categories.sort((a, b) => a.instant - b.instant);
|
|
16600
|
+
}
|
|
16601
|
+
const series = [];
|
|
16602
|
+
for (const source of sources) {
|
|
16603
|
+
for (const s of source.series) {
|
|
16604
|
+
series.push({
|
|
16605
|
+
sourceKey: source.sourceKey,
|
|
16606
|
+
name: s.name,
|
|
16607
|
+
data: categories.map(c => (s.values.has(c.key) ? s.values.get(c.key) : null))
|
|
16608
|
+
});
|
|
16609
|
+
}
|
|
16610
|
+
}
|
|
16611
|
+
return { categories, series };
|
|
16612
|
+
}
|
|
16613
|
+
/** Makes labels unique: a repeated label gets ` (2)`, ` (3)` …, so every category keeps its own slot. */
|
|
16614
|
+
function uniqueLabels(labels) {
|
|
16615
|
+
const seen = new Map();
|
|
16616
|
+
return labels.map(label => {
|
|
16617
|
+
const n = (seen.get(label) ?? 0) + 1;
|
|
16618
|
+
seen.set(label, n);
|
|
16619
|
+
return n === 1 ? label : `${label} (${n})`;
|
|
16620
|
+
});
|
|
16621
|
+
}
|
|
16622
|
+
|
|
16014
16623
|
const CHART_TYPE_MAPPING = {
|
|
16015
16624
|
column: { type: 'column', stack: false },
|
|
16016
16625
|
bar: { type: 'bar', stack: false },
|
|
@@ -16031,6 +16640,7 @@ class BarChartWidgetComponent {
|
|
|
16031
16640
|
]);
|
|
16032
16641
|
stateService = inject(MeshBoardStateService);
|
|
16033
16642
|
variableService = inject(MeshBoardVariableService);
|
|
16643
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
16034
16644
|
config;
|
|
16035
16645
|
// Widget state signals
|
|
16036
16646
|
_isLoading = signal(false, /* @ts-ignore */
|
|
@@ -16045,6 +16655,13 @@ class BarChartWidgetComponent {
|
|
|
16045
16655
|
categories = this._categories.asReadonly();
|
|
16046
16656
|
seriesData = this._seriesData.asReadonly();
|
|
16047
16657
|
error = this._error.asReadonly();
|
|
16658
|
+
// Multi-source path (AB#7953): per-source hints and whether the last load went through it.
|
|
16659
|
+
_sourceHints = signal([], /* @ts-ignore */
|
|
16660
|
+
...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
|
|
16661
|
+
sourceHints = this._sourceHints.asReadonly();
|
|
16662
|
+
_multiSource = signal(false, /* @ts-ignore */
|
|
16663
|
+
...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
|
|
16664
|
+
isMultiSource = this._multiSource.asReadonly();
|
|
16048
16665
|
data = computed(() => this._seriesData(), /* @ts-ignore */
|
|
16049
16666
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
16050
16667
|
/**
|
|
@@ -16130,6 +16747,27 @@ class BarChartWidgetComponent {
|
|
|
16130
16747
|
isDynamicSeriesMode() {
|
|
16131
16748
|
return !!(this.config.seriesGroupField && this.config.valueField);
|
|
16132
16749
|
}
|
|
16750
|
+
/** Whether the chart reads series from more than its primary query or has formula series (AB#7953). */
|
|
16751
|
+
hasAdditionalSeries() {
|
|
16752
|
+
return (this.config?.additionalSeries ?? []).some(s => this.isLoadableQuerySeries(s) || isCompleteFormulaSeries(s));
|
|
16753
|
+
}
|
|
16754
|
+
isLoadableQuerySeries(s) {
|
|
16755
|
+
return isCompleteQuerySeries(s) && !!s.categoryField;
|
|
16756
|
+
}
|
|
16757
|
+
/**
|
|
16758
|
+
* Shared tooltip rows of the multi-source path (AB#7953): one row per drawn series for the
|
|
16759
|
+
* hovered category; no bar (null) shows "–".
|
|
16760
|
+
*/
|
|
16761
|
+
tooltipRows(category) {
|
|
16762
|
+
const index = this._categories().indexOf(category);
|
|
16763
|
+
if (index < 0)
|
|
16764
|
+
return [];
|
|
16765
|
+
return this._seriesData().map(s => {
|
|
16766
|
+
const raw = s.data[index];
|
|
16767
|
+
const value = raw !== null && typeof raw === 'object' ? raw.value : raw;
|
|
16768
|
+
return { name: s.name, text: value === null || value === undefined ? '–' : this.formatValue(value) };
|
|
16769
|
+
});
|
|
16770
|
+
}
|
|
16133
16771
|
formatValue(value) {
|
|
16134
16772
|
return value.toLocaleString('de-AT', {
|
|
16135
16773
|
minimumFractionDigits: 0,
|
|
@@ -16141,6 +16779,12 @@ class BarChartWidgetComponent {
|
|
|
16141
16779
|
if (this.isNotConfigured()) {
|
|
16142
16780
|
return;
|
|
16143
16781
|
}
|
|
16782
|
+
if (this.hasAdditionalSeries()) {
|
|
16783
|
+
await this.loadMultiSource();
|
|
16784
|
+
return;
|
|
16785
|
+
}
|
|
16786
|
+
this._multiSource.set(false);
|
|
16787
|
+
this._sourceHints.set([]);
|
|
16144
16788
|
// Note: isNotConfigured() check ensures dataSource and queryRtId are set
|
|
16145
16789
|
const queryDataSource = this.config.dataSource;
|
|
16146
16790
|
this._isLoading.set(true);
|
|
@@ -16184,6 +16828,141 @@ class BarChartWidgetComponent {
|
|
|
16184
16828
|
this._isLoading.set(false);
|
|
16185
16829
|
}
|
|
16186
16830
|
}
|
|
16831
|
+
/**
|
|
16832
|
+
* Multi-source load (AB#7953): the primary query plus every complete query series, each with its
|
|
16833
|
+
* own scope and time-filter opt-out, matched per category value.
|
|
16834
|
+
*
|
|
16835
|
+
* 1. Scope per source: any `pending` → stay loading, no query; `empty` → no series for that
|
|
16836
|
+
* source and a "No data for <source>" hint.
|
|
16837
|
+
* 2. All sources load in parallel; a failing source gives an error hint, the others render.
|
|
16838
|
+
* 3. `valueScale` is applied to every source value, then {@link alignOnCategories} builds the
|
|
16839
|
+
* union of categories (chronological when all are date-times); a category a source lacks is
|
|
16840
|
+
* no bar for its series.
|
|
16841
|
+
* 4. `composeChartSeries` (shared with the line chart) evaluates formula series per category,
|
|
16842
|
+
* drops hidden series and adds "depends on <source>, which failed" hints.
|
|
16843
|
+
*/
|
|
16844
|
+
async loadMultiSource() {
|
|
16845
|
+
const primaryDs = this.config.dataSource;
|
|
16846
|
+
const sources = [
|
|
16847
|
+
{ key: PRIMARY_SOURCE_KEY, label: primaryDs.queryName?.trim() || 'Primary query', dataSource: primaryDs, primary: true },
|
|
16848
|
+
...(this.config.additionalSeries ?? [])
|
|
16849
|
+
.filter((s) => this.isLoadableQuerySeries(s))
|
|
16850
|
+
.map(def => ({
|
|
16851
|
+
key: def.key,
|
|
16852
|
+
label: querySeriesLabel(def),
|
|
16853
|
+
dataSource: def.dataSource,
|
|
16854
|
+
primary: false,
|
|
16855
|
+
categoryField: def.categoryField,
|
|
16856
|
+
valueField: def.valueField
|
|
16857
|
+
}))
|
|
16858
|
+
];
|
|
16859
|
+
this._isLoading.set(true);
|
|
16860
|
+
this._error.set(null);
|
|
16861
|
+
this._multiSource.set(true);
|
|
16862
|
+
const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
|
|
16863
|
+
if (scopes.some(s => s.kind === 'pending')) {
|
|
16864
|
+
return;
|
|
16865
|
+
}
|
|
16866
|
+
try {
|
|
16867
|
+
const hints = [];
|
|
16868
|
+
const problems = new Map();
|
|
16869
|
+
const noData = (source) => {
|
|
16870
|
+
hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
|
|
16871
|
+
problems.set(source.key, { label: source.label, problem: 'noData' });
|
|
16872
|
+
};
|
|
16873
|
+
const active = [];
|
|
16874
|
+
sources.forEach((source, i) => {
|
|
16875
|
+
const scope = scopes[i];
|
|
16876
|
+
if (scope.kind === 'empty')
|
|
16877
|
+
noData(source);
|
|
16878
|
+
else
|
|
16879
|
+
active.push({ source, rtIds: scope.kind === 'scoped' ? scope.rtIds : undefined });
|
|
16880
|
+
});
|
|
16881
|
+
const fetched = await Promise.allSettled(active.map(a => this.fetchSourceRows(a.source, a.rtIds)));
|
|
16882
|
+
const valueScale = this.config.valueScale ?? 1;
|
|
16883
|
+
const categorySources = [];
|
|
16884
|
+
fetched.forEach((f, i) => {
|
|
16885
|
+
const source = active[i].source;
|
|
16886
|
+
if (f.status === 'rejected') {
|
|
16887
|
+
console.error(`Error loading Bar Chart source "${source.label}":`, f.reason);
|
|
16888
|
+
hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
|
|
16889
|
+
problems.set(source.key, { label: source.label, problem: 'error' });
|
|
16890
|
+
return;
|
|
16891
|
+
}
|
|
16892
|
+
const converted = source.primary
|
|
16893
|
+
? primaryRowsToCategorySeries(f.value, this.config, valueScale)
|
|
16894
|
+
: (r => ({ categories: r.categories, series: [r.series] }))(queryRowsToCategorySeries(f.value, source.label, source.categoryField, source.valueField, valueScale));
|
|
16895
|
+
if (converted.categories.length === 0 || converted.series.length === 0) {
|
|
16896
|
+
noData(source);
|
|
16897
|
+
return;
|
|
16898
|
+
}
|
|
16899
|
+
categorySources.push({ sourceKey: source.key, sourceLabel: source.label, ...converted });
|
|
16900
|
+
});
|
|
16901
|
+
const aligned = alignOnCategories(categorySources);
|
|
16902
|
+
const composed = composeChartSeries({
|
|
16903
|
+
gridSeries: aligned.series,
|
|
16904
|
+
length: aligned.categories.length,
|
|
16905
|
+
definitions: this.config.additionalSeries ?? [],
|
|
16906
|
+
primary: this.config.primarySeries,
|
|
16907
|
+
stackedChart: false,
|
|
16908
|
+
sourceProblems: problems,
|
|
16909
|
+
evaluator: this.expressionEvaluator
|
|
16910
|
+
});
|
|
16911
|
+
hints.push(...composed.hints);
|
|
16912
|
+
const colors = new Map((this.config.series ?? []).map(s => [s.name ?? s.field, s.color]));
|
|
16913
|
+
this._categories.set(this.categoryLabels(aligned.categories));
|
|
16914
|
+
this._seriesData.set(composed.series.map(s => ({
|
|
16915
|
+
name: s.name,
|
|
16916
|
+
data: this.hasColorThresholds() ? this.applyColorThresholds(s.data) : s.data,
|
|
16917
|
+
color: colors.get(s.name)
|
|
16918
|
+
})));
|
|
16919
|
+
this._sourceHints.set(hints);
|
|
16920
|
+
this._isLoading.set(false);
|
|
16921
|
+
}
|
|
16922
|
+
catch (err) {
|
|
16923
|
+
console.error('Error loading Bar Chart data:', err);
|
|
16924
|
+
this._error.set('Failed to load data');
|
|
16925
|
+
this._sourceHints.set([]);
|
|
16926
|
+
this._isLoading.set(false);
|
|
16927
|
+
}
|
|
16928
|
+
}
|
|
16929
|
+
/** Rows of one source of the multi-source path; widget filters apply to the primary query only. */
|
|
16930
|
+
async fetchSourceRows(source, scopeRtIds) {
|
|
16931
|
+
const ds = source.dataSource;
|
|
16932
|
+
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
16933
|
+
const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
|
|
16934
|
+
const streamDataArgs = timeArgs || rtIds ? { ...timeArgs, rtIds } : undefined;
|
|
16935
|
+
const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
|
|
16936
|
+
fieldFilter: source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined,
|
|
16937
|
+
streamDataArgs,
|
|
16938
|
+
scopeRtIds
|
|
16939
|
+
}));
|
|
16940
|
+
return result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
|
|
16941
|
+
}
|
|
16942
|
+
/**
|
|
16943
|
+
* Category labels of the multi-source path: date-times as dates when every category is a
|
|
16944
|
+
* different day (daily bars), else date and time as the single-query chart shows them; other
|
|
16945
|
+
* values as text. Repeated labels get " (2)" so every category keeps its slot.
|
|
16946
|
+
*/
|
|
16947
|
+
categoryLabels(categories) {
|
|
16948
|
+
const mode = this.stateService.timeZoneMode();
|
|
16949
|
+
const allInstants = categories.length > 0 && categories.every(c => c.instant !== null);
|
|
16950
|
+
const dayOf = (c) => formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '';
|
|
16951
|
+
const byDay = allInstants && new Set(categories.map(dayOf)).size === categories.length;
|
|
16952
|
+
return uniqueLabels(categories.map(c => {
|
|
16953
|
+
if (byDay)
|
|
16954
|
+
return dayOf(c);
|
|
16955
|
+
if (c.instant !== null) {
|
|
16956
|
+
return formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) ?? String(c.raw);
|
|
16957
|
+
}
|
|
16958
|
+
return String(c.raw ?? '');
|
|
16959
|
+
}));
|
|
16960
|
+
}
|
|
16961
|
+
/** Short message of a failed source for its hint. */
|
|
16962
|
+
errorMessage(reason) {
|
|
16963
|
+
const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
|
|
16964
|
+
return message ? `Failed to load data (${message})` : 'Failed to load data';
|
|
16965
|
+
}
|
|
16187
16966
|
buildStreamDataArgs() {
|
|
16188
16967
|
const ds = this.config.dataSource;
|
|
16189
16968
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
@@ -16214,15 +16993,16 @@ class BarChartWidgetComponent {
|
|
|
16214
16993
|
for (const seriesConfig of (this.config.series ?? [])) {
|
|
16215
16994
|
const seriesCell = findCellForField(row.cells, seriesConfig.field);
|
|
16216
16995
|
if (seriesCell) {
|
|
16217
|
-
|
|
16218
|
-
rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
|
|
16996
|
+
rowValues.set(seriesConfig.field, BarChartWidgetComponent.toBarValue(seriesCell.value));
|
|
16219
16997
|
}
|
|
16220
16998
|
}
|
|
16221
16999
|
if (categoryValue !== '') {
|
|
16222
17000
|
categories.push(categoryValue);
|
|
16223
17001
|
// Add values for each series
|
|
16224
17002
|
for (const seriesConfig of (this.config.series ?? [])) {
|
|
16225
|
-
|
|
17003
|
+
// A null cell (no data, e.g. an Integral over an empty day) stays null: no bar
|
|
17004
|
+
// instead of a 0 bar (AB#7975). A missing cell is still 0.
|
|
17005
|
+
const value = rowValues.has(seriesConfig.field) ? rowValues.get(seriesConfig.field) : 0;
|
|
16226
17006
|
seriesMap.get(seriesConfig.field)?.push(value);
|
|
16227
17007
|
}
|
|
16228
17008
|
}
|
|
@@ -16269,13 +17049,7 @@ class BarChartWidgetComponent {
|
|
|
16269
17049
|
const valueCell = findCellForField(row.cells, valueField);
|
|
16270
17050
|
const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
|
|
16271
17051
|
const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
|
|
16272
|
-
|
|
16273
|
-
if (valueCell) {
|
|
16274
|
-
const val = valueCell.value;
|
|
16275
|
-
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
16276
|
-
if (isNaN(numericValue))
|
|
16277
|
-
numericValue = 0;
|
|
16278
|
-
}
|
|
17052
|
+
const numericValue = valueCell ? BarChartWidgetComponent.toBarValue(valueCell.value) : 0;
|
|
16279
17053
|
if (categoryValue && seriesGroupValue) {
|
|
16280
17054
|
allCategories.add(categoryValue);
|
|
16281
17055
|
allSeriesGroups.add(seriesGroupValue);
|
|
@@ -16292,7 +17066,12 @@ class BarChartWidgetComponent {
|
|
|
16292
17066
|
const valueScale = this.config?.valueScale ?? 1;
|
|
16293
17067
|
const seriesData = seriesGroups.map(seriesGroup => {
|
|
16294
17068
|
const rawData = categories.map(category => {
|
|
16295
|
-
|
|
17069
|
+
const cell = dataMap.get(category);
|
|
17070
|
+
// Missing combination → 0; a null cell (no data, AB#7975) → no bar.
|
|
17071
|
+
if (!cell?.has(seriesGroup))
|
|
17072
|
+
return 0;
|
|
17073
|
+
const value = cell.get(seriesGroup);
|
|
17074
|
+
return value === null ? null : value * valueScale;
|
|
16296
17075
|
});
|
|
16297
17076
|
return {
|
|
16298
17077
|
name: seriesGroup,
|
|
@@ -16317,11 +17096,21 @@ class BarChartWidgetComponent {
|
|
|
16317
17096
|
applyColorThresholds(data) {
|
|
16318
17097
|
const thresholds = this.config.colorThresholds ?? [];
|
|
16319
17098
|
const defaultColor = this.config.defaultBarColor ?? '#6b7280';
|
|
16320
|
-
return data.map(val => ({
|
|
17099
|
+
return data.map(val => val === null ? { value: null } : ({
|
|
16321
17100
|
value: val,
|
|
16322
17101
|
_color: this.getColorForValue(val, thresholds, defaultColor)
|
|
16323
17102
|
}));
|
|
16324
17103
|
}
|
|
17104
|
+
/**
|
|
17105
|
+
* Bar value of a cell: a number, `null` for a null cell ("no data" — Kendo draws no
|
|
17106
|
+
* bar, AB#7975), and 0 for text that is not a number.
|
|
17107
|
+
*/
|
|
17108
|
+
static toBarValue(value) {
|
|
17109
|
+
if (value === null || value === undefined)
|
|
17110
|
+
return null;
|
|
17111
|
+
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
|
17112
|
+
return isNaN(numValue) ? 0 : numValue;
|
|
17113
|
+
}
|
|
16325
17114
|
getColorForValue(val, thresholds, defaultColor) {
|
|
16326
17115
|
for (const t of thresholds) {
|
|
16327
17116
|
if (val <= t.value)
|
|
@@ -16365,6 +17154,15 @@ class BarChartWidgetComponent {
|
|
|
16365
17154
|
<span>{{ error() }}</span>
|
|
16366
17155
|
</div>
|
|
16367
17156
|
} @else {
|
|
17157
|
+
@if (sourceHints().length > 0) {
|
|
17158
|
+
<ul class="source-hints" aria-live="polite">
|
|
17159
|
+
@for (hint of sourceHints(); track hint.text) {
|
|
17160
|
+
<li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
|
|
17161
|
+
@if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
|
|
17162
|
+
</li>
|
|
17163
|
+
}
|
|
17164
|
+
</ul>
|
|
17165
|
+
}
|
|
16368
17166
|
<kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
|
|
16369
17167
|
<kendo-chart-area [background]="'transparent'"></kendo-chart-area>
|
|
16370
17168
|
<kendo-chart-category-axis>
|
|
@@ -16417,18 +17215,32 @@ class BarChartWidgetComponent {
|
|
|
16417
17215
|
[labels]="{ color: chartTheme().text }">
|
|
16418
17216
|
</kendo-chart-legend>
|
|
16419
17217
|
|
|
16420
|
-
|
|
16421
|
-
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
17218
|
+
@if (isMultiSource()) {
|
|
17219
|
+
<!-- Multi-source (AB#7953): one tooltip per category with every drawn series; no bar shows "–". -->
|
|
17220
|
+
<kendo-chart-tooltip [shared]="true">
|
|
17221
|
+
<ng-template kendoChartSharedTooltipTemplate let-category="category">
|
|
17222
|
+
<div class="chart-tooltip">
|
|
17223
|
+
<strong>{{ category }}</strong>
|
|
17224
|
+
@for (row of tooltipRows(category); track row.name) {
|
|
17225
|
+
<br/>{{ row.name }}: {{ row.text }}
|
|
17226
|
+
}
|
|
17227
|
+
</div>
|
|
17228
|
+
</ng-template>
|
|
17229
|
+
</kendo-chart-tooltip>
|
|
17230
|
+
} @else {
|
|
17231
|
+
<kendo-chart-tooltip>
|
|
17232
|
+
<ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
|
|
17233
|
+
<div class="chart-tooltip">
|
|
17234
|
+
<strong>{{ category }}</strong><br/>
|
|
17235
|
+
{{ series.name }}: {{ formatValue(value) }}
|
|
17236
|
+
</div>
|
|
17237
|
+
</ng-template>
|
|
17238
|
+
</kendo-chart-tooltip>
|
|
17239
|
+
}
|
|
16428
17240
|
</kendo-chart>
|
|
16429
17241
|
}
|
|
16430
17242
|
</div>
|
|
16431
|
-
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
17243
|
+
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
16432
17244
|
}
|
|
16433
17245
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartWidgetComponent, decorators: [{
|
|
16434
17246
|
type: Component,
|
|
@@ -16449,6 +17261,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
16449
17261
|
<span>{{ error() }}</span>
|
|
16450
17262
|
</div>
|
|
16451
17263
|
} @else {
|
|
17264
|
+
@if (sourceHints().length > 0) {
|
|
17265
|
+
<ul class="source-hints" aria-live="polite">
|
|
17266
|
+
@for (hint of sourceHints(); track hint.text) {
|
|
17267
|
+
<li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
|
|
17268
|
+
@if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
|
|
17269
|
+
</li>
|
|
17270
|
+
}
|
|
17271
|
+
</ul>
|
|
17272
|
+
}
|
|
16452
17273
|
<kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
|
|
16453
17274
|
<kendo-chart-area [background]="'transparent'"></kendo-chart-area>
|
|
16454
17275
|
<kendo-chart-category-axis>
|
|
@@ -16501,22 +17322,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
16501
17322
|
[labels]="{ color: chartTheme().text }">
|
|
16502
17323
|
</kendo-chart-legend>
|
|
16503
17324
|
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
17325
|
+
@if (isMultiSource()) {
|
|
17326
|
+
<!-- Multi-source (AB#7953): one tooltip per category with every drawn series; no bar shows "–". -->
|
|
17327
|
+
<kendo-chart-tooltip [shared]="true">
|
|
17328
|
+
<ng-template kendoChartSharedTooltipTemplate let-category="category">
|
|
17329
|
+
<div class="chart-tooltip">
|
|
17330
|
+
<strong>{{ category }}</strong>
|
|
17331
|
+
@for (row of tooltipRows(category); track row.name) {
|
|
17332
|
+
<br/>{{ row.name }}: {{ row.text }}
|
|
17333
|
+
}
|
|
17334
|
+
</div>
|
|
17335
|
+
</ng-template>
|
|
17336
|
+
</kendo-chart-tooltip>
|
|
17337
|
+
} @else {
|
|
17338
|
+
<kendo-chart-tooltip>
|
|
17339
|
+
<ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
|
|
17340
|
+
<div class="chart-tooltip">
|
|
17341
|
+
<strong>{{ category }}</strong><br/>
|
|
17342
|
+
{{ series.name }}: {{ formatValue(value) }}
|
|
17343
|
+
</div>
|
|
17344
|
+
</ng-template>
|
|
17345
|
+
</kendo-chart-tooltip>
|
|
17346
|
+
}
|
|
16512
17347
|
</kendo-chart>
|
|
16513
17348
|
}
|
|
16514
17349
|
</div>
|
|
16515
|
-
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
|
|
17350
|
+
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"] }]
|
|
16516
17351
|
}], propDecorators: { config: [{
|
|
16517
17352
|
type: Input
|
|
16518
17353
|
}] } });
|
|
16519
17354
|
|
|
17355
|
+
const NUMERIC_TYPES = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
|
|
17356
|
+
/**
|
|
17357
|
+
* Series section of the line chart config dialog (AB#7951, wireframe linked in AB#7951): the
|
|
17358
|
+
* series that come after the primary query's series, in legend / stacking order.
|
|
17359
|
+
*
|
|
17360
|
+
* - Query rows: own stream-data query, value field, entity-selector scope, time-filter opt-out and
|
|
17361
|
+
* resolution-aware routing (AB#7951).
|
|
17362
|
+
* - Formula rows (AB#7952): a formula over the other rows' keys in the shared formula field
|
|
17363
|
+
* (series keys as available names, no preview).
|
|
17364
|
+
* - Every row and the primary row: Visible and Stacked (Stacked only in a stacked area chart;
|
|
17365
|
+
* unchecked = drawn as a line over the stack).
|
|
17366
|
+
*
|
|
17367
|
+
* Rows can be added, removed and moved by keyboard; the focus stays on a moved row's button.
|
|
17368
|
+
*
|
|
17369
|
+
* `alignment="category"` (bar chart, AB#7953): rows are matched per category value, so every query
|
|
17370
|
+
* row gets its own category field; Resolution-aware and Stacked are not shown (the chart type
|
|
17371
|
+
* stacks).
|
|
17372
|
+
*/
|
|
17373
|
+
class LineChartSeriesEditorComponent {
|
|
17374
|
+
queryExecutor = inject(QueryExecutorService);
|
|
17375
|
+
stateService = inject(MeshBoardStateService);
|
|
17376
|
+
cdr = inject(ChangeDetectorRef);
|
|
17377
|
+
host = inject(ElementRef);
|
|
17378
|
+
evaluator = inject(ExpressionEvaluatorService);
|
|
17379
|
+
/** The series (query and formula rows), in order. */
|
|
17380
|
+
series = [];
|
|
17381
|
+
/** Display options of the primary query's series (AB#7952). */
|
|
17382
|
+
primary;
|
|
17383
|
+
/** The chart stacks (area + stacked): enables the per-series Stacked checkboxes (AB#7952). */
|
|
17384
|
+
stackedChart = false;
|
|
17385
|
+
/** Emits the primary display options on change; `undefined` when both are default. */
|
|
17386
|
+
primaryChange = new EventEmitter();
|
|
17387
|
+
/** Entity selectors of the board (scope picker). */
|
|
17388
|
+
selectors = [];
|
|
17389
|
+
/**
|
|
17390
|
+
* How the chart matches series (AB#7953): `timeGrid` (line chart, default) or `category` (bar
|
|
17391
|
+
* chart: per-row category field, no Resolution-aware, no Stacked).
|
|
17392
|
+
*/
|
|
17393
|
+
alignment = 'timeGrid';
|
|
17394
|
+
/** Category field preset for a new query row in category mode (the primary query's category field). */
|
|
17395
|
+
defaultCategoryField;
|
|
17396
|
+
/** Emits a new array on every change. */
|
|
17397
|
+
seriesChange = new EventEmitter();
|
|
17398
|
+
rowCards;
|
|
17399
|
+
get arrowUpIcon() { return arrowUpIcon; }
|
|
17400
|
+
get arrowDownIcon() { return arrowDownIcon; }
|
|
17401
|
+
get plusIcon() { return plusIcon; }
|
|
17402
|
+
get trashIcon() { return MM_ACTION_ICONS.delete; }
|
|
17403
|
+
streamDataOnly = ['streamData'];
|
|
17404
|
+
stackedDisabledHint = 'Only a stacked area chart stacks series.';
|
|
17405
|
+
keySyntaxHint = '${key}';
|
|
17406
|
+
formulaPlaceholder = 'e.g. min(${pv} - ${gridExport}, ${consumption})';
|
|
17407
|
+
aggregationOptions = [
|
|
17408
|
+
{ value: 'SUM', label: 'Sum' },
|
|
17409
|
+
{ value: 'AVG', label: 'Average' },
|
|
17410
|
+
{ value: 'MAX', label: 'Max' },
|
|
17411
|
+
{ value: 'MIN', label: 'Min' },
|
|
17412
|
+
{ value: 'COUNT', label: 'Count' },
|
|
17413
|
+
{ value: 'TIME_WEIGHTED_AVG', label: 'Time-weighted average' }
|
|
17414
|
+
];
|
|
17415
|
+
/** Rows are matched per category value (bar chart, AB#7953). */
|
|
17416
|
+
get categoryMode() {
|
|
17417
|
+
return this.alignment === 'category';
|
|
17418
|
+
}
|
|
17419
|
+
rows = [];
|
|
17420
|
+
errors = [];
|
|
17421
|
+
nextUid = 1;
|
|
17422
|
+
operandCache = new Map();
|
|
17423
|
+
/** The array last emitted, so an echo through `[series]` does not rebuild the rows. */
|
|
17424
|
+
lastEmitted = null;
|
|
17425
|
+
ngOnChanges(changes) {
|
|
17426
|
+
if (changes['series'] && this.series !== this.lastEmitted) {
|
|
17427
|
+
this.rows = (this.series ?? []).map(s => this.createRow(structuredClone(s)));
|
|
17428
|
+
this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
|
|
17429
|
+
for (const row of this.rows) {
|
|
17430
|
+
if (row.series.kind === 'query' && row.series.dataSource.queryRtId)
|
|
17431
|
+
void this.loadColumns(row);
|
|
17432
|
+
}
|
|
17433
|
+
}
|
|
17434
|
+
else if (changes['alignment']) {
|
|
17435
|
+
this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
|
|
17436
|
+
}
|
|
17437
|
+
}
|
|
17438
|
+
add() {
|
|
17439
|
+
this.rows = [...this.rows, this.createRow({
|
|
17440
|
+
kind: 'query',
|
|
17441
|
+
key: '',
|
|
17442
|
+
dataSource: { type: 'persistentQuery', queryRtId: '', queryFamily: 'streamData' },
|
|
17443
|
+
valueField: ''
|
|
17444
|
+
})];
|
|
17445
|
+
this.emit();
|
|
17446
|
+
this.focusAfterRender(this.rows.length - 1, 'key');
|
|
17447
|
+
}
|
|
17448
|
+
addFormula() {
|
|
17449
|
+
this.rows = [...this.rows, this.createRow({ kind: 'formula', key: '', formula: '' })];
|
|
17450
|
+
this.emit();
|
|
17451
|
+
this.focusAfterRender(this.rows.length - 1, 'key');
|
|
17452
|
+
}
|
|
17453
|
+
/** The row as query series (template narrowing). */
|
|
17454
|
+
asQuery(row) {
|
|
17455
|
+
return row.series.kind === 'query' ? row.series : null;
|
|
17456
|
+
}
|
|
17457
|
+
/** The row as formula series (template narrowing). */
|
|
17458
|
+
asFormula(row) {
|
|
17459
|
+
return row.series.kind === 'formula' ? row.series : null;
|
|
17460
|
+
}
|
|
17461
|
+
/**
|
|
17462
|
+
* Names a formula row can insert: the keys of every other row (a self-reference would be a
|
|
17463
|
+
* circular reference). Cached per key set, so the formula field's inputs keep their reference.
|
|
17464
|
+
*/
|
|
17465
|
+
operandsFor(row) {
|
|
17466
|
+
const names = this.rows
|
|
17467
|
+
.filter(r => r !== row && SERIES_KEY_PATTERN.test(r.series.key?.trim() ?? ''))
|
|
17468
|
+
.map(r => r.series.key.trim());
|
|
17469
|
+
const cacheKey = names.join('\u0000');
|
|
17470
|
+
const cached = this.operandCache.get(row.uid);
|
|
17471
|
+
if (cached && cached.key === cacheKey)
|
|
17472
|
+
return cached.variables;
|
|
17473
|
+
const variables = names.map(name => ({ name, type: 'number', source: 'static', value: '' }));
|
|
17474
|
+
this.operandCache.set(row.uid, { key: cacheKey, variables });
|
|
17475
|
+
return variables;
|
|
17476
|
+
}
|
|
17477
|
+
/**
|
|
17478
|
+
* Message under a row. For a formula row the shared formula field already shows syntax and
|
|
17479
|
+
* unknown-key messages; the row repeats only what the field cannot know (key, circular reference).
|
|
17480
|
+
*/
|
|
17481
|
+
rowMessage(index) {
|
|
17482
|
+
const message = this.errors[index] ?? null;
|
|
17483
|
+
const series = this.rows[index]?.series;
|
|
17484
|
+
if (!message || series?.kind !== 'formula' || !series.formula?.trim())
|
|
17485
|
+
return message;
|
|
17486
|
+
const keys = this.rows.map(r => r.series.key?.trim() ?? '').filter(k => !!k);
|
|
17487
|
+
const fieldError = validateFormula(series.formula, keys, this.evaluator).error;
|
|
17488
|
+
return message === fieldError ? null : message;
|
|
17489
|
+
}
|
|
17490
|
+
setOption(row, option, on) {
|
|
17491
|
+
const next = { ...row.series };
|
|
17492
|
+
if (on)
|
|
17493
|
+
delete next[option];
|
|
17494
|
+
else
|
|
17495
|
+
next[option] = false;
|
|
17496
|
+
row.series = next;
|
|
17497
|
+
this.emit();
|
|
17498
|
+
}
|
|
17499
|
+
setPrimaryOption(option, on) {
|
|
17500
|
+
const next = { ...(this.primary ?? {}) };
|
|
17501
|
+
if (on)
|
|
17502
|
+
delete next[option];
|
|
17503
|
+
else
|
|
17504
|
+
next[option] = false;
|
|
17505
|
+
this.primary = Object.keys(next).length > 0 ? next : undefined;
|
|
17506
|
+
this.primaryChange.emit(this.primary);
|
|
17507
|
+
this.cdr.markForCheck();
|
|
17508
|
+
}
|
|
17509
|
+
remove(index) {
|
|
17510
|
+
this.rows = this.rows.filter((_, i) => i !== index);
|
|
17511
|
+
this.emit();
|
|
17512
|
+
if (this.rows.length > 0)
|
|
17513
|
+
this.focusAfterRender(Math.min(index, this.rows.length - 1), 'down');
|
|
17514
|
+
else
|
|
17515
|
+
this.focusAdd();
|
|
17516
|
+
}
|
|
17517
|
+
move(index, delta) {
|
|
17518
|
+
const target = index + delta;
|
|
17519
|
+
if (target < 0 || target >= this.rows.length)
|
|
17520
|
+
return;
|
|
17521
|
+
const rows = [...this.rows];
|
|
17522
|
+
[rows[index], rows[target]] = [rows[target], rows[index]];
|
|
17523
|
+
this.rows = rows;
|
|
17524
|
+
this.emit();
|
|
17525
|
+
// Keep the focus on the moved row: on its button in the same direction, or the other one at an end.
|
|
17526
|
+
const atEnd = delta < 0 ? target === 0 : target === rows.length - 1;
|
|
17527
|
+
this.focusAfterRender(target, atEnd ? (delta < 0 ? 'down' : 'up') : (delta < 0 ? 'up' : 'down'));
|
|
17528
|
+
}
|
|
17529
|
+
async onQuerySelected(row, query) {
|
|
17530
|
+
if (row.series.kind !== 'query')
|
|
17531
|
+
return;
|
|
17532
|
+
row.query = query;
|
|
17533
|
+
row.series.dataSource = {
|
|
17534
|
+
...row.series.dataSource,
|
|
17535
|
+
queryRtId: query?.rtId ?? '',
|
|
17536
|
+
queryName: query?.name || undefined,
|
|
17537
|
+
queryFamily: 'streamData'
|
|
17538
|
+
};
|
|
17539
|
+
row.series.valueField = '';
|
|
17540
|
+
if (this.categoryMode)
|
|
17541
|
+
row.series.categoryField = '';
|
|
17542
|
+
row.numericColumns = [];
|
|
17543
|
+
row.allColumns = [];
|
|
17544
|
+
this.emit();
|
|
17545
|
+
if (query) {
|
|
17546
|
+
await this.loadColumns(row);
|
|
17547
|
+
this.presetCategoryField(row);
|
|
17548
|
+
}
|
|
17549
|
+
}
|
|
17550
|
+
/**
|
|
17551
|
+
* Category mode: a row without a category field gets the primary query's category field when
|
|
17552
|
+
* its query has that column, else `timestamp` when it has one (AB#7953).
|
|
17553
|
+
*/
|
|
17554
|
+
presetCategoryField(row) {
|
|
17555
|
+
if (!this.categoryMode || row.series.kind !== 'query' || row.series.categoryField)
|
|
17556
|
+
return;
|
|
17557
|
+
const paths = row.allColumns.map(c => c.attributePath);
|
|
17558
|
+
const preset = [this.defaultCategoryField, 'timestamp'].find(f => !!f && paths.includes(f));
|
|
17559
|
+
if (preset) {
|
|
17560
|
+
row.series.categoryField = preset;
|
|
17561
|
+
this.emit();
|
|
17562
|
+
}
|
|
17563
|
+
}
|
|
17564
|
+
validationOptions() {
|
|
17565
|
+
return { requireCategoryField: this.categoryMode };
|
|
17566
|
+
}
|
|
17567
|
+
onResolutionAwareChange(row, on) {
|
|
17568
|
+
if (row.series.kind !== 'query')
|
|
17569
|
+
return;
|
|
17570
|
+
row.series.dataSource = {
|
|
17571
|
+
...row.series.dataSource,
|
|
17572
|
+
resolutionAware: on || undefined,
|
|
17573
|
+
requiredAggregation: on ? (row.series.dataSource.requiredAggregation ?? 'AVG') : undefined
|
|
17574
|
+
};
|
|
17575
|
+
this.emit();
|
|
17576
|
+
}
|
|
17577
|
+
emit() {
|
|
17578
|
+
const series = this.rows.map(r => ({ ...r.series, key: r.series.key?.trim() ?? '', label: r.series.label?.trim() || undefined }));
|
|
17579
|
+
this.errors = querySeriesErrors(series, this.evaluator, this.validationOptions());
|
|
17580
|
+
this.lastEmitted = series;
|
|
17581
|
+
this.seriesChange.emit(series);
|
|
17582
|
+
this.cdr.markForCheck();
|
|
17583
|
+
}
|
|
17584
|
+
createRow(series) {
|
|
17585
|
+
const ds = series.kind === 'query' ? series.dataSource : undefined;
|
|
17586
|
+
return {
|
|
17587
|
+
uid: this.nextUid++,
|
|
17588
|
+
series,
|
|
17589
|
+
query: ds?.queryRtId ? { rtId: ds.queryRtId, name: ds.queryName || ds.queryRtId } : null,
|
|
17590
|
+
numericColumns: [],
|
|
17591
|
+
allColumns: [],
|
|
17592
|
+
loadingColumns: false
|
|
17593
|
+
};
|
|
17594
|
+
}
|
|
17595
|
+
/** Numeric columns of the row's query, probed with the board window (AB#7975). */
|
|
17596
|
+
async loadColumns(row) {
|
|
17597
|
+
if (row.series.kind !== 'query')
|
|
17598
|
+
return;
|
|
17599
|
+
const queryRtId = row.series.dataSource.queryRtId;
|
|
17600
|
+
row.loadingColumns = true;
|
|
17601
|
+
this.cdr.markForCheck();
|
|
17602
|
+
try {
|
|
17603
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(queryRtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
|
|
17604
|
+
row.allColumns = columns
|
|
17605
|
+
.map(c => ({ attributePath: c.attributePath, attributeValueType: c.attributeValueType ?? '', aggregationType: c.aggregationType ?? null }));
|
|
17606
|
+
row.numericColumns = row.allColumns.filter(c => NUMERIC_TYPES.includes(c.attributeValueType));
|
|
17607
|
+
}
|
|
17608
|
+
catch (error) {
|
|
17609
|
+
console.warn('Could not load the columns of the series query:', error);
|
|
17610
|
+
row.numericColumns = [];
|
|
17611
|
+
row.allColumns = [];
|
|
17612
|
+
}
|
|
17613
|
+
finally {
|
|
17614
|
+
row.loadingColumns = false;
|
|
17615
|
+
this.cdr.markForCheck();
|
|
17616
|
+
}
|
|
17617
|
+
}
|
|
17618
|
+
focusAfterRender(index, target) {
|
|
17619
|
+
setTimeout(() => {
|
|
17620
|
+
const card = this.rowCards?.get(index)?.nativeElement;
|
|
17621
|
+
const selector = target === 'key' ? `[data-series-key="${index}"] input` : `[data-series-action="${target}"]`;
|
|
17622
|
+
const el = card?.querySelector(selector);
|
|
17623
|
+
(el && !el.disabled ? el : card)?.focus();
|
|
17624
|
+
});
|
|
17625
|
+
}
|
|
17626
|
+
focusAdd() {
|
|
17627
|
+
setTimeout(() => this.host.nativeElement.querySelector('[data-series-action="add"]')?.focus());
|
|
17628
|
+
}
|
|
17629
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17630
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartSeriesEditorComponent, isStandalone: true, selector: "mm-line-chart-series-editor", inputs: { series: "series", primary: "primary", stackedChart: "stackedChart", selectors: "selectors", alignment: "alignment", defaultCategoryField: "defaultCategoryField" }, outputs: { primaryChange: "primaryChange", seriesChange: "seriesChange" }, viewQueries: [{ propertyName: "rowCards", predicate: ["rowCard"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
17631
|
+
<div class="series-editor">
|
|
17632
|
+
<div class="series-row primary" role="group" aria-label="Series of the primary query">
|
|
17633
|
+
<span class="tag">primary</span>
|
|
17634
|
+
<span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
|
|
17635
|
+
<span class="display-options">
|
|
17636
|
+
<label class="checkbox">
|
|
17637
|
+
<input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
|
|
17638
|
+
(ngModelChange)="setPrimaryOption('visible', $event)" />
|
|
17639
|
+
Visible
|
|
17640
|
+
</label>
|
|
17641
|
+
@if (!categoryMode) {
|
|
17642
|
+
<label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
|
|
17643
|
+
<input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
|
|
17644
|
+
[ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
|
|
17645
|
+
Stacked
|
|
17646
|
+
</label>
|
|
17647
|
+
}
|
|
17648
|
+
</span>
|
|
17649
|
+
</div>
|
|
17650
|
+
|
|
17651
|
+
@for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
|
|
17652
|
+
<div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
|
|
17653
|
+
<div class="row-actions">
|
|
17654
|
+
<button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
|
|
17655
|
+
data-series-action="up" [disabled]="first"
|
|
17656
|
+
[attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
|
|
17657
|
+
(click)="move(i, -1)"></button>
|
|
17658
|
+
<button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
|
|
17659
|
+
data-series-action="down" [disabled]="last"
|
|
17660
|
+
[attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
|
|
17661
|
+
(click)="move(i, 1)"></button>
|
|
17662
|
+
<span class="tag">{{ row.series.kind }}</span>
|
|
17663
|
+
</div>
|
|
17664
|
+
|
|
17665
|
+
<div class="field key">
|
|
17666
|
+
<label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
|
|
17667
|
+
<kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
|
|
17668
|
+
[(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
|
|
17669
|
+
</div>
|
|
17670
|
+
<div class="field label">
|
|
17671
|
+
<label [for]="'series-label-' + row.uid">Label</label>
|
|
17672
|
+
<kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
|
|
17673
|
+
(valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
|
|
17674
|
+
</div>
|
|
17675
|
+
@if (asQuery(row); as q) {
|
|
17676
|
+
<div class="field query">
|
|
17677
|
+
<label>Query (stream data) <span class="required">*</span></label>
|
|
17678
|
+
<mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
|
|
17679
|
+
(querySelected)="onQuerySelected(row, $event)"
|
|
17680
|
+
placeholder="Select a stream-data query..."></mm-query-selector>
|
|
17681
|
+
</div>
|
|
17682
|
+
@if (categoryMode) {
|
|
17683
|
+
<div class="field category">
|
|
17684
|
+
<label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
|
|
17685
|
+
<kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
|
|
17686
|
+
[textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
|
|
17687
|
+
[loading]="row.loadingColumns" [allowCustom]="true"
|
|
17688
|
+
[(ngModel)]="q.categoryField" (valueChange)="emit()"
|
|
17689
|
+
placeholder="e.g. timestamp"></kendo-combobox>
|
|
17690
|
+
</div>
|
|
17691
|
+
}
|
|
17692
|
+
<div class="field value">
|
|
17693
|
+
<label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
|
|
17694
|
+
<kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
|
|
17695
|
+
[textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
|
|
17696
|
+
[loading]="row.loadingColumns" [allowCustom]="true"
|
|
17697
|
+
[(ngModel)]="q.valueField" (valueChange)="emit()"
|
|
17698
|
+
placeholder="Numeric column..."></kendo-combobox>
|
|
17699
|
+
</div>
|
|
17700
|
+
<div class="field scope">
|
|
17701
|
+
<mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
|
|
17702
|
+
[entitySelectorId]="q.dataSource.entitySelectorId"
|
|
17703
|
+
(entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
|
|
17704
|
+
</mm-entity-selector-scope-picker>
|
|
17705
|
+
</div>
|
|
17706
|
+
<div class="field toggles">
|
|
17707
|
+
<mm-sd-time-filter-toggle [family]="'streamData'"
|
|
17708
|
+
[ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
|
|
17709
|
+
(ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
|
|
17710
|
+
</mm-sd-time-filter-toggle>
|
|
17711
|
+
@if (!categoryMode) {
|
|
17712
|
+
<label class="checkbox">
|
|
17713
|
+
<input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
|
|
17714
|
+
(ngModelChange)="onResolutionAwareChange(row, $event)" />
|
|
17715
|
+
Resolution-aware
|
|
17716
|
+
</label>
|
|
17717
|
+
}
|
|
17718
|
+
@if (!categoryMode && q.dataSource.resolutionAware) {
|
|
17719
|
+
<kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
|
|
17720
|
+
[valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
|
|
17721
|
+
[(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
|
|
17722
|
+
</kendo-dropdownlist>
|
|
17723
|
+
}
|
|
17724
|
+
</div>
|
|
17725
|
+
} @else if (asFormula(row); as f) {
|
|
17726
|
+
<div class="field formula">
|
|
17727
|
+
<mm-meshboard-formula-field
|
|
17728
|
+
[formula]="f.formula"
|
|
17729
|
+
(formulaChange)="f.formula = $event; emit()"
|
|
17730
|
+
[variables]="operandsFor(row)"
|
|
17731
|
+
[showPreview]="false"
|
|
17732
|
+
label="Formula"
|
|
17733
|
+
variablesLabel="Available series"
|
|
17734
|
+
namesNoun="series"
|
|
17735
|
+
[syntaxHint]="keySyntaxHint"
|
|
17736
|
+
emptyVariablesText="No other series yet. Add query series first."
|
|
17737
|
+
[placeholder]="formulaPlaceholder">
|
|
17738
|
+
</mm-meshboard-formula-field>
|
|
17739
|
+
</div>
|
|
17740
|
+
}
|
|
17741
|
+
<div class="field display-options">
|
|
17742
|
+
<label class="checkbox">
|
|
17743
|
+
<input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
|
|
17744
|
+
(ngModelChange)="setOption(row, 'visible', $event)" />
|
|
17745
|
+
Visible
|
|
17746
|
+
</label>
|
|
17747
|
+
@if (!categoryMode) {
|
|
17748
|
+
<label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
|
|
17749
|
+
<input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
|
|
17750
|
+
[ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
|
|
17751
|
+
Stacked
|
|
17752
|
+
</label>
|
|
17753
|
+
}
|
|
17754
|
+
</div>
|
|
17755
|
+
<button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
|
|
17756
|
+
data-series-action="remove"
|
|
17757
|
+
[attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
|
|
17758
|
+
(click)="remove(i)"></button>
|
|
17759
|
+
@if (rowMessage(i); as message) {
|
|
17760
|
+
<p class="row-error" role="alert">{{ message }}</p>
|
|
17761
|
+
}
|
|
17762
|
+
</div>
|
|
17763
|
+
}
|
|
17764
|
+
|
|
17765
|
+
<div class="add-actions">
|
|
17766
|
+
<button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
|
|
17767
|
+
Add query series
|
|
17768
|
+
</button>
|
|
17769
|
+
<button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
|
|
17770
|
+
Add formula series
|
|
17771
|
+
</button>
|
|
17772
|
+
</div>
|
|
17773
|
+
</div>
|
|
17774
|
+
`, isInline: true, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17775
|
+
}
|
|
17776
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, decorators: [{
|
|
17777
|
+
type: Component,
|
|
17778
|
+
args: [{ selector: 'mm-line-chart-series-editor', standalone: true, imports: [
|
|
17779
|
+
FormsModule,
|
|
17780
|
+
ButtonModule,
|
|
17781
|
+
DropDownsModule,
|
|
17782
|
+
InputsModule,
|
|
17783
|
+
QuerySelectorComponent,
|
|
17784
|
+
SdTimeFilterToggleComponent,
|
|
17785
|
+
EntitySelectorScopePickerComponent,
|
|
17786
|
+
FormulaFieldComponent
|
|
17787
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
17788
|
+
<div class="series-editor">
|
|
17789
|
+
<div class="series-row primary" role="group" aria-label="Series of the primary query">
|
|
17790
|
+
<span class="tag">primary</span>
|
|
17791
|
+
<span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
|
|
17792
|
+
<span class="display-options">
|
|
17793
|
+
<label class="checkbox">
|
|
17794
|
+
<input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
|
|
17795
|
+
(ngModelChange)="setPrimaryOption('visible', $event)" />
|
|
17796
|
+
Visible
|
|
17797
|
+
</label>
|
|
17798
|
+
@if (!categoryMode) {
|
|
17799
|
+
<label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
|
|
17800
|
+
<input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
|
|
17801
|
+
[ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
|
|
17802
|
+
Stacked
|
|
17803
|
+
</label>
|
|
17804
|
+
}
|
|
17805
|
+
</span>
|
|
17806
|
+
</div>
|
|
17807
|
+
|
|
17808
|
+
@for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
|
|
17809
|
+
<div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
|
|
17810
|
+
<div class="row-actions">
|
|
17811
|
+
<button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
|
|
17812
|
+
data-series-action="up" [disabled]="first"
|
|
17813
|
+
[attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
|
|
17814
|
+
(click)="move(i, -1)"></button>
|
|
17815
|
+
<button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
|
|
17816
|
+
data-series-action="down" [disabled]="last"
|
|
17817
|
+
[attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
|
|
17818
|
+
(click)="move(i, 1)"></button>
|
|
17819
|
+
<span class="tag">{{ row.series.kind }}</span>
|
|
17820
|
+
</div>
|
|
17821
|
+
|
|
17822
|
+
<div class="field key">
|
|
17823
|
+
<label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
|
|
17824
|
+
<kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
|
|
17825
|
+
[(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
|
|
17826
|
+
</div>
|
|
17827
|
+
<div class="field label">
|
|
17828
|
+
<label [for]="'series-label-' + row.uid">Label</label>
|
|
17829
|
+
<kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
|
|
17830
|
+
(valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
|
|
17831
|
+
</div>
|
|
17832
|
+
@if (asQuery(row); as q) {
|
|
17833
|
+
<div class="field query">
|
|
17834
|
+
<label>Query (stream data) <span class="required">*</span></label>
|
|
17835
|
+
<mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
|
|
17836
|
+
(querySelected)="onQuerySelected(row, $event)"
|
|
17837
|
+
placeholder="Select a stream-data query..."></mm-query-selector>
|
|
17838
|
+
</div>
|
|
17839
|
+
@if (categoryMode) {
|
|
17840
|
+
<div class="field category">
|
|
17841
|
+
<label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
|
|
17842
|
+
<kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
|
|
17843
|
+
[textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
|
|
17844
|
+
[loading]="row.loadingColumns" [allowCustom]="true"
|
|
17845
|
+
[(ngModel)]="q.categoryField" (valueChange)="emit()"
|
|
17846
|
+
placeholder="e.g. timestamp"></kendo-combobox>
|
|
17847
|
+
</div>
|
|
17848
|
+
}
|
|
17849
|
+
<div class="field value">
|
|
17850
|
+
<label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
|
|
17851
|
+
<kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
|
|
17852
|
+
[textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
|
|
17853
|
+
[loading]="row.loadingColumns" [allowCustom]="true"
|
|
17854
|
+
[(ngModel)]="q.valueField" (valueChange)="emit()"
|
|
17855
|
+
placeholder="Numeric column..."></kendo-combobox>
|
|
17856
|
+
</div>
|
|
17857
|
+
<div class="field scope">
|
|
17858
|
+
<mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
|
|
17859
|
+
[entitySelectorId]="q.dataSource.entitySelectorId"
|
|
17860
|
+
(entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
|
|
17861
|
+
</mm-entity-selector-scope-picker>
|
|
17862
|
+
</div>
|
|
17863
|
+
<div class="field toggles">
|
|
17864
|
+
<mm-sd-time-filter-toggle [family]="'streamData'"
|
|
17865
|
+
[ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
|
|
17866
|
+
(ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
|
|
17867
|
+
</mm-sd-time-filter-toggle>
|
|
17868
|
+
@if (!categoryMode) {
|
|
17869
|
+
<label class="checkbox">
|
|
17870
|
+
<input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
|
|
17871
|
+
(ngModelChange)="onResolutionAwareChange(row, $event)" />
|
|
17872
|
+
Resolution-aware
|
|
17873
|
+
</label>
|
|
17874
|
+
}
|
|
17875
|
+
@if (!categoryMode && q.dataSource.resolutionAware) {
|
|
17876
|
+
<kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
|
|
17877
|
+
[valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
|
|
17878
|
+
[(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
|
|
17879
|
+
</kendo-dropdownlist>
|
|
17880
|
+
}
|
|
17881
|
+
</div>
|
|
17882
|
+
} @else if (asFormula(row); as f) {
|
|
17883
|
+
<div class="field formula">
|
|
17884
|
+
<mm-meshboard-formula-field
|
|
17885
|
+
[formula]="f.formula"
|
|
17886
|
+
(formulaChange)="f.formula = $event; emit()"
|
|
17887
|
+
[variables]="operandsFor(row)"
|
|
17888
|
+
[showPreview]="false"
|
|
17889
|
+
label="Formula"
|
|
17890
|
+
variablesLabel="Available series"
|
|
17891
|
+
namesNoun="series"
|
|
17892
|
+
[syntaxHint]="keySyntaxHint"
|
|
17893
|
+
emptyVariablesText="No other series yet. Add query series first."
|
|
17894
|
+
[placeholder]="formulaPlaceholder">
|
|
17895
|
+
</mm-meshboard-formula-field>
|
|
17896
|
+
</div>
|
|
17897
|
+
}
|
|
17898
|
+
<div class="field display-options">
|
|
17899
|
+
<label class="checkbox">
|
|
17900
|
+
<input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
|
|
17901
|
+
(ngModelChange)="setOption(row, 'visible', $event)" />
|
|
17902
|
+
Visible
|
|
17903
|
+
</label>
|
|
17904
|
+
@if (!categoryMode) {
|
|
17905
|
+
<label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
|
|
17906
|
+
<input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
|
|
17907
|
+
[ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
|
|
17908
|
+
Stacked
|
|
17909
|
+
</label>
|
|
17910
|
+
}
|
|
17911
|
+
</div>
|
|
17912
|
+
<button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
|
|
17913
|
+
data-series-action="remove"
|
|
17914
|
+
[attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
|
|
17915
|
+
(click)="remove(i)"></button>
|
|
17916
|
+
@if (rowMessage(i); as message) {
|
|
17917
|
+
<p class="row-error" role="alert">{{ message }}</p>
|
|
17918
|
+
}
|
|
17919
|
+
</div>
|
|
17920
|
+
}
|
|
17921
|
+
|
|
17922
|
+
<div class="add-actions">
|
|
17923
|
+
<button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
|
|
17924
|
+
Add query series
|
|
17925
|
+
</button>
|
|
17926
|
+
<button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
|
|
17927
|
+
Add formula series
|
|
17928
|
+
</button>
|
|
17929
|
+
</div>
|
|
17930
|
+
</div>
|
|
17931
|
+
`, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"] }]
|
|
17932
|
+
}], propDecorators: { series: [{
|
|
17933
|
+
type: Input
|
|
17934
|
+
}], primary: [{
|
|
17935
|
+
type: Input
|
|
17936
|
+
}], stackedChart: [{
|
|
17937
|
+
type: Input
|
|
17938
|
+
}], primaryChange: [{
|
|
17939
|
+
type: Output
|
|
17940
|
+
}], selectors: [{
|
|
17941
|
+
type: Input
|
|
17942
|
+
}], alignment: [{
|
|
17943
|
+
type: Input
|
|
17944
|
+
}], defaultCategoryField: [{
|
|
17945
|
+
type: Input
|
|
17946
|
+
}], seriesChange: [{
|
|
17947
|
+
type: Output
|
|
17948
|
+
}], rowCards: [{
|
|
17949
|
+
type: ViewChildren,
|
|
17950
|
+
args: ['rowCard']
|
|
17951
|
+
}] } });
|
|
17952
|
+
|
|
16520
17953
|
/**
|
|
16521
17954
|
* Configuration dialog for Bar Chart widgets.
|
|
16522
17955
|
* Supports column, bar, stacked, and 100% stacked chart types.
|
|
@@ -16526,6 +17959,7 @@ class BarChartConfigDialogComponent {
|
|
|
16526
17959
|
queryExecutor = inject(QueryExecutorService);
|
|
16527
17960
|
stateService = inject(MeshBoardStateService);
|
|
16528
17961
|
windowRef = inject(WindowRef);
|
|
17962
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
16529
17963
|
querySelector;
|
|
16530
17964
|
// Initial values for editing
|
|
16531
17965
|
initialQueryRtId;
|
|
@@ -16544,6 +17978,16 @@ class BarChartConfigDialogComponent {
|
|
|
16544
17978
|
initialColorThresholds;
|
|
16545
17979
|
initialDefaultBarColor;
|
|
16546
17980
|
initialFilters;
|
|
17981
|
+
initialAdditionalSeries;
|
|
17982
|
+
initialPrimarySeries;
|
|
17983
|
+
/** Query and formula series of the Series section (AB#7953). */
|
|
17984
|
+
additionalSeries = [];
|
|
17985
|
+
/** Display options of the primary query's series (AB#7953). */
|
|
17986
|
+
primarySeries;
|
|
17987
|
+
/** The series that are saved: none unless the primary query is a stream-data query. */
|
|
17988
|
+
get effectiveAdditionalSeries() {
|
|
17989
|
+
return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
|
|
17990
|
+
}
|
|
16547
17991
|
// State
|
|
16548
17992
|
isLoadingInitial = false;
|
|
16549
17993
|
isLoadingColumns = false;
|
|
@@ -16599,6 +18043,10 @@ class BarChartConfigDialogComponent {
|
|
|
16599
18043
|
if (this.selectedPersistentQuery === null || this.form.categoryField === '') {
|
|
16600
18044
|
return false;
|
|
16601
18045
|
}
|
|
18046
|
+
const seriesValid = querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator, { requireCategoryField: true })
|
|
18047
|
+
.every(e => e === null);
|
|
18048
|
+
if (!seriesValid)
|
|
18049
|
+
return false;
|
|
16602
18050
|
if (this.seriesMode === 'dynamic') {
|
|
16603
18051
|
return this.seriesGroupField !== '' && this.valueField !== '';
|
|
16604
18052
|
}
|
|
@@ -16623,6 +18071,8 @@ class BarChartConfigDialogComponent {
|
|
|
16623
18071
|
this.form.showDataLabels = this.initialShowDataLabels ?? false;
|
|
16624
18072
|
this.form.colorThresholds = this.initialColorThresholds ? [...this.initialColorThresholds] : [];
|
|
16625
18073
|
this.form.defaultBarColor = this.initialDefaultBarColor ?? '';
|
|
18074
|
+
this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
|
|
18075
|
+
this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
|
|
16626
18076
|
// Determine series mode from initial values
|
|
16627
18077
|
if (this.initialSeriesGroupField && this.initialValueField) {
|
|
16628
18078
|
this.seriesMode = 'dynamic';
|
|
@@ -16730,8 +18180,10 @@ class BarChartConfigDialogComponent {
|
|
|
16730
18180
|
aggregationType: c.aggregationType ?? null
|
|
16731
18181
|
}));
|
|
16732
18182
|
}
|
|
16733
|
-
|
|
16734
|
-
|
|
18183
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
18184
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
18185
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
|
|
18186
|
+
return columns.map(c => ({
|
|
16735
18187
|
attributePath: c.attributePath,
|
|
16736
18188
|
attributeValueType: c.attributeValueType ?? '',
|
|
16737
18189
|
aggregationType: c.aggregationType ?? null
|
|
@@ -16783,7 +18235,9 @@ class BarChartConfigDialogComponent {
|
|
|
16783
18235
|
showDataLabels: this.form.showDataLabels,
|
|
16784
18236
|
colorThresholds: this.form.colorThresholds.length > 0 ? this.form.colorThresholds : undefined,
|
|
16785
18237
|
defaultBarColor: this.form.defaultBarColor || undefined,
|
|
16786
|
-
filters: filtersDto
|
|
18238
|
+
filters: filtersDto,
|
|
18239
|
+
additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
|
|
18240
|
+
primarySeries: this.selectedQueryFamily === 'streamData' && this.primarySeries?.visible === false ? { visible: false } : undefined
|
|
16787
18241
|
};
|
|
16788
18242
|
// Add dynamic series fields if in dynamic mode
|
|
16789
18243
|
if (this.seriesMode === 'dynamic') {
|
|
@@ -16802,7 +18256,7 @@ class BarChartConfigDialogComponent {
|
|
|
16802
18256
|
this.windowRef.close();
|
|
16803
18257
|
}
|
|
16804
18258
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16805
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
18259
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
16806
18260
|
<div class="config-container">
|
|
16807
18261
|
|
|
16808
18262
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -16941,6 +18395,23 @@ class BarChartConfigDialogComponent {
|
|
|
16941
18395
|
</div>
|
|
16942
18396
|
}
|
|
16943
18397
|
|
|
18398
|
+
<!-- Series Section (AB#7953): query and formula series matched per category -->
|
|
18399
|
+
@if (selectedQueryFamily === 'streamData') {
|
|
18400
|
+
<div class="config-section">
|
|
18401
|
+
<h3 class="section-title">Series</h3>
|
|
18402
|
+
<p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
|
|
18403
|
+
<mm-line-chart-series-editor
|
|
18404
|
+
alignment="category"
|
|
18405
|
+
[series]="additionalSeries"
|
|
18406
|
+
[selectors]="availableEntitySelectors"
|
|
18407
|
+
[primary]="primarySeries"
|
|
18408
|
+
[defaultCategoryField]="form.categoryField"
|
|
18409
|
+
(seriesChange)="additionalSeries = $event"
|
|
18410
|
+
(primaryChange)="primarySeries = $event">
|
|
18411
|
+
</mm-line-chart-series-editor>
|
|
18412
|
+
</div>
|
|
18413
|
+
}
|
|
18414
|
+
|
|
16944
18415
|
<!-- Filters Section -->
|
|
16945
18416
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
16946
18417
|
<div class="config-section">
|
|
@@ -17074,7 +18545,7 @@ class BarChartConfigDialogComponent {
|
|
|
17074
18545
|
</button>
|
|
17075
18546
|
</div>
|
|
17076
18547
|
</div>
|
|
17077
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
18548
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
17078
18549
|
}
|
|
17079
18550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
|
|
17080
18551
|
type: Component,
|
|
@@ -17089,7 +18560,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17089
18560
|
QuerySelectorComponent,
|
|
17090
18561
|
SdTimeFilterToggleComponent,
|
|
17091
18562
|
EntitySelectorScopePickerComponent,
|
|
17092
|
-
LoadingOverlayComponent
|
|
18563
|
+
LoadingOverlayComponent,
|
|
18564
|
+
LineChartSeriesEditorComponent
|
|
17093
18565
|
], template: `
|
|
17094
18566
|
<div class="config-container">
|
|
17095
18567
|
|
|
@@ -17229,6 +18701,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17229
18701
|
</div>
|
|
17230
18702
|
}
|
|
17231
18703
|
|
|
18704
|
+
<!-- Series Section (AB#7953): query and formula series matched per category -->
|
|
18705
|
+
@if (selectedQueryFamily === 'streamData') {
|
|
18706
|
+
<div class="config-section">
|
|
18707
|
+
<h3 class="section-title">Series</h3>
|
|
18708
|
+
<p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
|
|
18709
|
+
<mm-line-chart-series-editor
|
|
18710
|
+
alignment="category"
|
|
18711
|
+
[series]="additionalSeries"
|
|
18712
|
+
[selectors]="availableEntitySelectors"
|
|
18713
|
+
[primary]="primarySeries"
|
|
18714
|
+
[defaultCategoryField]="form.categoryField"
|
|
18715
|
+
(seriesChange)="additionalSeries = $event"
|
|
18716
|
+
(primaryChange)="primarySeries = $event">
|
|
18717
|
+
</mm-line-chart-series-editor>
|
|
18718
|
+
</div>
|
|
18719
|
+
}
|
|
18720
|
+
|
|
17232
18721
|
<!-- Filters Section -->
|
|
17233
18722
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
17234
18723
|
<div class="config-section">
|
|
@@ -17398,8 +18887,179 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17398
18887
|
type: Input
|
|
17399
18888
|
}], initialFilters: [{
|
|
17400
18889
|
type: Input
|
|
18890
|
+
}], initialAdditionalSeries: [{
|
|
18891
|
+
type: Input
|
|
18892
|
+
}], initialPrimarySeries: [{
|
|
18893
|
+
type: Input
|
|
17401
18894
|
}] } });
|
|
17402
18895
|
|
|
18896
|
+
/**
|
|
18897
|
+
* Aligns the series of several stream-data sources on one time grid (AB#7951).
|
|
18898
|
+
*
|
|
18899
|
+
* Every source delivers its points keyed by the bucket start instant (epoch ms). Each source's
|
|
18900
|
+
* bucket step is its most frequent distance between consecutive instants (gaps in the data do not
|
|
18901
|
+
* change it). The grid step is the COARSEST source step; the source that has it (the first one on a
|
|
18902
|
+
* tie, i.e. the primary source when it is passed first) is the reference and gives the anchor.
|
|
18903
|
+
*
|
|
18904
|
+
* - Fixed-size grid (step up to one hour): a source is aligned when every instant is a whole number
|
|
18905
|
+
* of grid steps away from the reference anchor. A finer rung (5-min buckets on a 15-min grid) or
|
|
18906
|
+
* a shifted anchor puts instants between grid points: that source is reported as misaligned and
|
|
18907
|
+
* left out instead of being plotted shifted. Missing grid instants between the first and the last
|
|
18908
|
+
* instant are filled in, so a bucket no source has data for is still a gap.
|
|
18909
|
+
* - Calendar grid (longer steps: days, months, whose real length changes with DST and month
|
|
18910
|
+
* length): a source is aligned when none of its instants lies strictly inside a reference bucket
|
|
18911
|
+
* (0 < distance to the nearest reference instant < step). Instants only, no wall-clock
|
|
18912
|
+
* arithmetic, so days across a DST change (23 h / 25 h) align when the sources share them. No
|
|
18913
|
+
* fill-in.
|
|
18914
|
+
*
|
|
18915
|
+
* Gaps: a grid instant a series has no point for is `null` (rendered as a gap, never 0).
|
|
18916
|
+
*/
|
|
18917
|
+
/** Largest fixed-size grid step: modular alignment check and fill-in up to here, calendar rule above. */
|
|
18918
|
+
const MAX_FILLED_GRID_STEP_MS = 60 * 60 * 1000;
|
|
18919
|
+
/** Instants a source needs for its step to count when the reference is chosen. */
|
|
18920
|
+
const MIN_REFERENCE_INSTANTS = 3;
|
|
18921
|
+
/** Upper bound of filled grid instants, guarding against a degenerate step over a long window. */
|
|
18922
|
+
const MAX_FILLED_GRID_POINTS = 20000;
|
|
18923
|
+
/** Sorted distinct instants of all series of a source. */
|
|
18924
|
+
function sourceInstants(source) {
|
|
18925
|
+
const set = new Set();
|
|
18926
|
+
for (const s of source.series) {
|
|
18927
|
+
for (const t of s.points.keys()) {
|
|
18928
|
+
if (Number.isFinite(t))
|
|
18929
|
+
set.add(t);
|
|
18930
|
+
}
|
|
18931
|
+
}
|
|
18932
|
+
return Array.from(set).sort((a, b) => a - b);
|
|
18933
|
+
}
|
|
18934
|
+
/**
|
|
18935
|
+
* Typical bucket step of sorted instants: the most frequent positive distance between consecutive
|
|
18936
|
+
* instants (the smaller one on a tie), `null` for fewer than two instants.
|
|
18937
|
+
*/
|
|
18938
|
+
function bucketStepMs(instants) {
|
|
18939
|
+
const counts = new Map();
|
|
18940
|
+
for (let i = 1; i < instants.length; i++) {
|
|
18941
|
+
const d = instants[i] - instants[i - 1];
|
|
18942
|
+
if (d > 0)
|
|
18943
|
+
counts.set(d, (counts.get(d) ?? 0) + 1);
|
|
18944
|
+
}
|
|
18945
|
+
let step = null;
|
|
18946
|
+
let best = 0;
|
|
18947
|
+
for (const [d, n] of counts) {
|
|
18948
|
+
if (n > best || (n === best && step !== null && d < step)) {
|
|
18949
|
+
step = d;
|
|
18950
|
+
best = n;
|
|
18951
|
+
}
|
|
18952
|
+
}
|
|
18953
|
+
return step;
|
|
18954
|
+
}
|
|
18955
|
+
/** Distance from `t` to the nearest value in the sorted array `sorted` (Infinity when empty). */
|
|
18956
|
+
function distanceToNearest(sorted, t) {
|
|
18957
|
+
let lo = 0;
|
|
18958
|
+
let hi = sorted.length - 1;
|
|
18959
|
+
if (hi < 0)
|
|
18960
|
+
return Infinity;
|
|
18961
|
+
while (lo < hi) {
|
|
18962
|
+
const mid = (lo + hi) >> 1;
|
|
18963
|
+
if (sorted[mid] < t)
|
|
18964
|
+
lo = mid + 1;
|
|
18965
|
+
else
|
|
18966
|
+
hi = mid;
|
|
18967
|
+
}
|
|
18968
|
+
let best = Math.abs(sorted[lo] - t);
|
|
18969
|
+
if (lo > 0)
|
|
18970
|
+
best = Math.min(best, Math.abs(sorted[lo - 1] - t));
|
|
18971
|
+
return best;
|
|
18972
|
+
}
|
|
18973
|
+
/**
|
|
18974
|
+
* Aligns the sources on one grid. Source order is kept for the series; the first source wins a
|
|
18975
|
+
* tie for the coarsest step (the primary source when it is passed first).
|
|
18976
|
+
*/
|
|
18977
|
+
function alignOnTimeGrid(sources) {
|
|
18978
|
+
const perSource = sources.map(source => {
|
|
18979
|
+
const instants = sourceInstants(source);
|
|
18980
|
+
return { source, instants, step: bucketStepMs(instants) };
|
|
18981
|
+
});
|
|
18982
|
+
// Reference = coarsest step; the first source wins a tie.
|
|
18983
|
+
// Reference = coarsest step; the first source wins a tie. A source with fewer than
|
|
18984
|
+
// MIN_REFERENCE_INSTANTS instants has no reliable step (two sparse points look like a coarse
|
|
18985
|
+
// rung) and is the reference only when no source has enough instants.
|
|
18986
|
+
const eligible = perSource.some(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
|
|
18987
|
+
? perSource.filter(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
|
|
18988
|
+
: perSource;
|
|
18989
|
+
let reference = null;
|
|
18990
|
+
for (const entry of eligible) {
|
|
18991
|
+
if (entry.step !== null && (reference === null || entry.step > (reference.step ?? 0))) {
|
|
18992
|
+
reference = entry;
|
|
18993
|
+
}
|
|
18994
|
+
}
|
|
18995
|
+
const gridStepMs = reference?.step ?? null;
|
|
18996
|
+
const misaligned = [];
|
|
18997
|
+
const aligned = [];
|
|
18998
|
+
for (const entry of perSource) {
|
|
18999
|
+
if (reference && gridStepMs !== null && entry !== reference) {
|
|
19000
|
+
const anchor = reference.instants[0];
|
|
19001
|
+
const off = gridStepMs <= MAX_FILLED_GRID_STEP_MS
|
|
19002
|
+
? entry.instants.some(t => (t - anchor) % gridStepMs !== 0)
|
|
19003
|
+
: entry.instants.some(t => {
|
|
19004
|
+
const d = distanceToNearest(reference.instants, t);
|
|
19005
|
+
return d > 0 && d < gridStepMs;
|
|
19006
|
+
});
|
|
19007
|
+
if (off) {
|
|
19008
|
+
misaligned.push({
|
|
19009
|
+
sourceKey: entry.source.sourceKey,
|
|
19010
|
+
sourceLabel: entry.source.sourceLabel,
|
|
19011
|
+
stepMs: entry.step,
|
|
19012
|
+
gridStepMs
|
|
19013
|
+
});
|
|
19014
|
+
continue;
|
|
19015
|
+
}
|
|
19016
|
+
}
|
|
19017
|
+
aligned.push(entry);
|
|
19018
|
+
}
|
|
19019
|
+
const instantSet = new Set();
|
|
19020
|
+
for (const entry of aligned) {
|
|
19021
|
+
for (const t of entry.instants)
|
|
19022
|
+
instantSet.add(t);
|
|
19023
|
+
}
|
|
19024
|
+
let instants = Array.from(instantSet).sort((a, b) => a - b);
|
|
19025
|
+
if (gridStepMs !== null && gridStepMs <= MAX_FILLED_GRID_STEP_MS && instants.length > 1) {
|
|
19026
|
+
instants = fillGrid(instants, gridStepMs);
|
|
19027
|
+
}
|
|
19028
|
+
const series = [];
|
|
19029
|
+
for (const entry of aligned) {
|
|
19030
|
+
for (const s of entry.source.series) {
|
|
19031
|
+
series.push({
|
|
19032
|
+
name: s.name,
|
|
19033
|
+
sourceKey: entry.source.sourceKey,
|
|
19034
|
+
data: instants.map(t => s.points.get(t) ?? null),
|
|
19035
|
+
unit: s.unit
|
|
19036
|
+
});
|
|
19037
|
+
}
|
|
19038
|
+
}
|
|
19039
|
+
return { instants, gridStepMs, series, misaligned };
|
|
19040
|
+
}
|
|
19041
|
+
/**
|
|
19042
|
+
* Adds the missing multiples of `step` between consecutive instants whose distance is a whole
|
|
19043
|
+
* multiple of it. Distances that are not a multiple (a source-own instant inside a reference gap)
|
|
19044
|
+
* are left as they are.
|
|
19045
|
+
*/
|
|
19046
|
+
function fillGrid(instants, step) {
|
|
19047
|
+
const span = instants[instants.length - 1] - instants[0];
|
|
19048
|
+
if (span / step > MAX_FILLED_GRID_POINTS)
|
|
19049
|
+
return instants;
|
|
19050
|
+
const out = [instants[0]];
|
|
19051
|
+
for (let i = 1; i < instants.length; i++) {
|
|
19052
|
+
const prev = instants[i - 1];
|
|
19053
|
+
const d = instants[i] - prev;
|
|
19054
|
+
if (d > step && d % step === 0) {
|
|
19055
|
+
for (let t = prev + step; t < instants[i]; t += step)
|
|
19056
|
+
out.push(t);
|
|
19057
|
+
}
|
|
19058
|
+
out.push(instants[i]);
|
|
19059
|
+
}
|
|
19060
|
+
return out;
|
|
19061
|
+
}
|
|
19062
|
+
|
|
17403
19063
|
/** Series colours so a series' min/max band and its avg line share one hue. */
|
|
17404
19064
|
const SERIES_PALETTE = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
|
|
17405
19065
|
/**
|
|
@@ -17429,6 +19089,7 @@ class LineChartWidgetComponent {
|
|
|
17429
19089
|
]);
|
|
17430
19090
|
stateService = inject(MeshBoardStateService);
|
|
17431
19091
|
variableService = inject(MeshBoardVariableService);
|
|
19092
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
17432
19093
|
config;
|
|
17433
19094
|
// Widget state signals
|
|
17434
19095
|
_isLoading = signal(false, /* @ts-ignore */
|
|
@@ -17466,6 +19127,10 @@ class LineChartWidgetComponent {
|
|
|
17466
19127
|
valueAxes = this._valueAxes.asReadonly();
|
|
17467
19128
|
error = this._error.asReadonly();
|
|
17468
19129
|
dataInfo = this._dataInfo.asReadonly();
|
|
19130
|
+
// Per-source hints of a multi-source chart (AB#7951); always empty for a single-query chart.
|
|
19131
|
+
_sourceHints = signal([], /* @ts-ignore */
|
|
19132
|
+
...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
|
|
19133
|
+
sourceHints = this._sourceHints.asReadonly();
|
|
17469
19134
|
/**
|
|
17470
19135
|
* Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
|
|
17471
19136
|
* `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
|
|
@@ -17608,6 +19273,49 @@ class LineChartWidgetComponent {
|
|
|
17608
19273
|
return this.config?.chartType ?? 'line';
|
|
17609
19274
|
}, /* @ts-ignore */
|
|
17610
19275
|
...(ngDevMode ? [{ debugName: "chartType" }] : /* istanbul ignore next */ []));
|
|
19276
|
+
/**
|
|
19277
|
+
* Stacking (AB#7951): only an area chart with `stacked` stacks; the series stack in their order
|
|
19278
|
+
* (primary series first, then `additionalSeries`). `false` is Kendo's default, so an existing
|
|
19279
|
+
* chart stacks exactly as before (not at all).
|
|
19280
|
+
*/
|
|
19281
|
+
stackSeries() {
|
|
19282
|
+
return !!this.config?.stacked && this.chartType() === 'area';
|
|
19283
|
+
}
|
|
19284
|
+
/**
|
|
19285
|
+
* Kendo `missingValues` (AB#7951), always set explicitly. A stacked or multi-source chart uses
|
|
19286
|
+
* `'gap'`: a bucket without data is a gap, never drawn or stacked as 0 and never bridged.
|
|
19287
|
+
* Single-query unstacked charts get exactly Kendo's defaults — `'interpolate'` for a line,
|
|
19288
|
+
* `'zero'` for an area series — so they render as before.
|
|
19289
|
+
*/
|
|
19290
|
+
missingValues() {
|
|
19291
|
+
if (this.stackSeries() || this.hasAdditionalSeries())
|
|
19292
|
+
return 'gap';
|
|
19293
|
+
return this.chartType() === 'area' ? 'zero' : 'interpolate';
|
|
19294
|
+
}
|
|
19295
|
+
/** Whether the chart reads series from more than its primary query (AB#7951) or has formula series (AB#7952). */
|
|
19296
|
+
hasAdditionalSeries() {
|
|
19297
|
+
return (this.config?.additionalSeries ?? []).some(s => isCompleteQuerySeries(s) || isCompleteFormulaSeries(s));
|
|
19298
|
+
}
|
|
19299
|
+
/** Whether the last load went through the multi-source path (aligned grid, AB#7951/AB#7952). */
|
|
19300
|
+
_multiSource = signal(false, /* @ts-ignore */
|
|
19301
|
+
...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
|
|
19302
|
+
isMultiSource = this._multiSource.asReadonly();
|
|
19303
|
+
/**
|
|
19304
|
+
* Shared tooltip rows of the multi-source path (AB#7952): one row per drawn series for the
|
|
19305
|
+
* hovered category; a gap (null) shows "–".
|
|
19306
|
+
*/
|
|
19307
|
+
tooltipRows(category) {
|
|
19308
|
+
const index = this._categories().indexOf(category);
|
|
19309
|
+
if (index < 0)
|
|
19310
|
+
return [];
|
|
19311
|
+
return this._seriesData().map(s => {
|
|
19312
|
+
const value = s.data[index];
|
|
19313
|
+
return {
|
|
19314
|
+
name: s.name,
|
|
19315
|
+
text: value === null || value === undefined ? '–' : `${this.formatValue(value)}${this.getUnitForSeries(s.name)}`
|
|
19316
|
+
};
|
|
19317
|
+
});
|
|
19318
|
+
}
|
|
17611
19319
|
labelRotation = computed(() => {
|
|
17612
19320
|
const categoryCount = this._categories().length;
|
|
17613
19321
|
return categoryCount > 5 ? -45 : 0;
|
|
@@ -17709,6 +19417,12 @@ class LineChartWidgetComponent {
|
|
|
17709
19417
|
async loadData() {
|
|
17710
19418
|
if (this.isNotConfigured())
|
|
17711
19419
|
return;
|
|
19420
|
+
this._multiSource.set(this.hasAdditionalSeries());
|
|
19421
|
+
if (this.hasAdditionalSeries()) {
|
|
19422
|
+
await this.loadMultiSource();
|
|
19423
|
+
return;
|
|
19424
|
+
}
|
|
19425
|
+
this._sourceHints.set([]);
|
|
17712
19426
|
const queryDataSource = this.config.dataSource;
|
|
17713
19427
|
this._isLoading.set(true);
|
|
17714
19428
|
this._error.set(null);
|
|
@@ -17890,6 +19604,240 @@ class LineChartWidgetComponent {
|
|
|
17890
19604
|
]
|
|
17891
19605
|
};
|
|
17892
19606
|
}
|
|
19607
|
+
/**
|
|
19608
|
+
* Multi-source load (AB#7951): the primary data source plus every complete query series, each
|
|
19609
|
+
* with its own scope, time-filter opt-out and resolution-aware routing, aligned on one time grid.
|
|
19610
|
+
*
|
|
19611
|
+
* 1. Scope: any source pending → stay loading, no query; an empty scope → the source has no
|
|
19612
|
+
* series and a "No data for <source>" hint.
|
|
19613
|
+
* 2. Prepare: resolution-aware sources resolve their rollup rung.
|
|
19614
|
+
* 3. Common bucket count: the smallest of the pixel-derived target and every resolved
|
|
19615
|
+
* `points` (= the coarsest effective bucket). Every source is downsampled with this count over
|
|
19616
|
+
* the same window, so all of them use the same bucket width.
|
|
19617
|
+
* 4. Fetch all sources in parallel; one failing source gives an error hint, the others render.
|
|
19618
|
+
* 5. {@link alignOnTimeGrid}: shared categories, gaps as null, misaligned sources left out with a hint.
|
|
19619
|
+
*/
|
|
19620
|
+
async loadMultiSource() {
|
|
19621
|
+
const primaryDs = this.config.dataSource;
|
|
19622
|
+
const sources = [
|
|
19623
|
+
{
|
|
19624
|
+
key: PRIMARY_SOURCE_KEY,
|
|
19625
|
+
label: primaryDs.queryName?.trim() || 'Primary query',
|
|
19626
|
+
dataSource: primaryDs,
|
|
19627
|
+
valueField: this.config.valueField,
|
|
19628
|
+
primary: true
|
|
19629
|
+
},
|
|
19630
|
+
...(this.config.additionalSeries ?? [])
|
|
19631
|
+
.filter(isCompleteQuerySeries)
|
|
19632
|
+
.map((def) => ({
|
|
19633
|
+
key: def.key,
|
|
19634
|
+
label: querySeriesLabel(def),
|
|
19635
|
+
dataSource: def.dataSource,
|
|
19636
|
+
valueField: def.valueField,
|
|
19637
|
+
primary: false
|
|
19638
|
+
}))
|
|
19639
|
+
];
|
|
19640
|
+
this._isLoading.set(true);
|
|
19641
|
+
this._error.set(null);
|
|
19642
|
+
const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
|
|
19643
|
+
if (scopes.some(s => s.kind === 'pending')) {
|
|
19644
|
+
return;
|
|
19645
|
+
}
|
|
19646
|
+
try {
|
|
19647
|
+
const hints = [];
|
|
19648
|
+
const problems = new Map();
|
|
19649
|
+
const active = [];
|
|
19650
|
+
sources.forEach((source, i) => {
|
|
19651
|
+
if (scopes[i].kind === 'empty') {
|
|
19652
|
+
hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
|
|
19653
|
+
problems.set(source.key, { label: source.label, problem: 'noData' });
|
|
19654
|
+
}
|
|
19655
|
+
else {
|
|
19656
|
+
active.push(source);
|
|
19657
|
+
}
|
|
19658
|
+
});
|
|
19659
|
+
const prepared = await Promise.allSettled(active.map(s => this.prepareSource(s)));
|
|
19660
|
+
let limit = this.computeDownsampleLimit();
|
|
19661
|
+
const targetPoints = limit;
|
|
19662
|
+
let signal = null;
|
|
19663
|
+
for (const p of prepared) {
|
|
19664
|
+
const resolution = p.status === 'fulfilled' ? p.value.resolutionAware?.resolution : null;
|
|
19665
|
+
if (resolution) {
|
|
19666
|
+
limit = Math.min(limit, Math.max(1, resolution.points));
|
|
19667
|
+
signal ??= resolution;
|
|
19668
|
+
}
|
|
19669
|
+
}
|
|
19670
|
+
const fetched = await Promise.allSettled(active.map((source, i) => {
|
|
19671
|
+
const p = prepared[i];
|
|
19672
|
+
return p.status === 'fulfilled'
|
|
19673
|
+
? this.fetchSource(source, p.value, limit)
|
|
19674
|
+
: Promise.reject(p.reason);
|
|
19675
|
+
}));
|
|
19676
|
+
const gridSources = [];
|
|
19677
|
+
let rowCount = 0;
|
|
19678
|
+
fetched.forEach((f, i) => {
|
|
19679
|
+
const source = active[i];
|
|
19680
|
+
if (f.status === 'rejected') {
|
|
19681
|
+
console.error(`Error loading Line Chart source "${source.label}":`, f.reason);
|
|
19682
|
+
hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
|
|
19683
|
+
problems.set(source.key, { label: source.label, problem: 'error' });
|
|
19684
|
+
return;
|
|
19685
|
+
}
|
|
19686
|
+
rowCount += f.value.rows.length;
|
|
19687
|
+
const series = source.primary
|
|
19688
|
+
? primaryRowsToSeries(f.value.rows, this.config, f.value.downsampled)
|
|
19689
|
+
: [queryRowsToSeries(f.value.rows, source.label, source.valueField, f.value.downsampled)];
|
|
19690
|
+
const withData = series.filter(s => s.points.size > 0);
|
|
19691
|
+
if (withData.length === 0) {
|
|
19692
|
+
hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
|
|
19693
|
+
problems.set(source.key, { label: source.label, problem: 'noData' });
|
|
19694
|
+
return;
|
|
19695
|
+
}
|
|
19696
|
+
gridSources.push({ sourceKey: source.key, sourceLabel: source.label, series: withData });
|
|
19697
|
+
});
|
|
19698
|
+
const grid = alignOnTimeGrid(gridSources);
|
|
19699
|
+
for (const m of grid.misaligned) {
|
|
19700
|
+
hints.push({
|
|
19701
|
+
kind: 'misaligned',
|
|
19702
|
+
sourceLabel: m.sourceLabel,
|
|
19703
|
+
text: `${m.sourceLabel}: its buckets do not match the chart's time grid and are not shown`
|
|
19704
|
+
});
|
|
19705
|
+
problems.set(m.sourceKey, { label: m.sourceLabel, problem: 'misaligned' });
|
|
19706
|
+
}
|
|
19707
|
+
// Formula series, per-series visibility and stacking (AB#7952).
|
|
19708
|
+
const composed = composeChartSeries({
|
|
19709
|
+
gridSeries: grid.series,
|
|
19710
|
+
length: grid.instants.length,
|
|
19711
|
+
definitions: this.config.additionalSeries ?? [],
|
|
19712
|
+
primary: this.config.primarySeries,
|
|
19713
|
+
stackedChart: this.stackSeries(),
|
|
19714
|
+
sourceProblems: problems,
|
|
19715
|
+
evaluator: this.expressionEvaluator
|
|
19716
|
+
});
|
|
19717
|
+
hints.push(...composed.hints);
|
|
19718
|
+
this.applyAlignedGrid(grid.instants, composed.series);
|
|
19719
|
+
this._sourceHints.set(hints);
|
|
19720
|
+
this._resolutionSignal.set(signal);
|
|
19721
|
+
this._resolutionTarget.set(signal ? targetPoints : null);
|
|
19722
|
+
this.lastLimit = limit;
|
|
19723
|
+
this._dataInfo.set({ rows: rowCount, points: grid.instants.length, total: rowCount });
|
|
19724
|
+
this._isLoading.set(false);
|
|
19725
|
+
}
|
|
19726
|
+
catch (err) {
|
|
19727
|
+
console.error('Error loading Line Chart data:', err);
|
|
19728
|
+
this._error.set('Failed to load data');
|
|
19729
|
+
this._dataInfo.set(null);
|
|
19730
|
+
this._resolutionSignal.set(null);
|
|
19731
|
+
this._sourceHints.set([]);
|
|
19732
|
+
this._isLoading.set(false);
|
|
19733
|
+
}
|
|
19734
|
+
}
|
|
19735
|
+
/** Resolution-aware sources resolve their rung first (step 2 of {@link loadMultiSource}). */
|
|
19736
|
+
async prepareSource(source) {
|
|
19737
|
+
const ds = source.dataSource;
|
|
19738
|
+
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
19739
|
+
const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
|
|
19740
|
+
if (!(ds.resolutionAware && ds.queryFamily === 'streamData' && source.valueField && timeArgs?.from && timeArgs?.to)) {
|
|
19741
|
+
return { timeArgs, rtIds };
|
|
19742
|
+
}
|
|
19743
|
+
const info = await this.queryExecutor.fetchQueryArchive(ds.queryRtId);
|
|
19744
|
+
if (!info) {
|
|
19745
|
+
throw new Error('base archive not found for the selected query');
|
|
19746
|
+
}
|
|
19747
|
+
const scopeRtIds = rtIds && rtIds.length > 0 ? rtIds : (info.rtIds ?? undefined);
|
|
19748
|
+
const resolution = await this.queryExecutor.resolveSeriesQuery({
|
|
19749
|
+
baseArchiveRtId: info.archiveRtId,
|
|
19750
|
+
from: timeArgs.from,
|
|
19751
|
+
to: timeArgs.to,
|
|
19752
|
+
targetPoints: this.computeDownsampleLimit(),
|
|
19753
|
+
requiredAggregation: (ds.requiredAggregation ?? 'SUM'),
|
|
19754
|
+
sourcePath: source.valueField,
|
|
19755
|
+
rtIds: scopeRtIds,
|
|
19756
|
+
timeZone: this.stateService.resolveStreamDataTimeZone()
|
|
19757
|
+
});
|
|
19758
|
+
return {
|
|
19759
|
+
timeArgs,
|
|
19760
|
+
rtIds,
|
|
19761
|
+
resolutionAware: { resolution, ckTypeId: info.ckTypeId, scopeRtIds, from: timeArgs.from, to: timeArgs.to }
|
|
19762
|
+
};
|
|
19763
|
+
}
|
|
19764
|
+
/** Fetches one source with the common bucket count (step 4 of {@link loadMultiSource}). */
|
|
19765
|
+
async fetchSource(source, prepared, limit) {
|
|
19766
|
+
const ds = source.dataSource;
|
|
19767
|
+
// Widget filters belong to the primary query's type; query series run unfiltered.
|
|
19768
|
+
const fieldFilter = source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined;
|
|
19769
|
+
const ra = prepared.resolutionAware;
|
|
19770
|
+
if (ra) {
|
|
19771
|
+
const resolution = ra.resolution;
|
|
19772
|
+
if (!resolution)
|
|
19773
|
+
return { rows: [], downsampled: true };
|
|
19774
|
+
const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
|
|
19775
|
+
archiveRtId: resolution.archiveRtId,
|
|
19776
|
+
from: ra.from,
|
|
19777
|
+
to: ra.to,
|
|
19778
|
+
limit,
|
|
19779
|
+
sourcePath: source.valueField,
|
|
19780
|
+
aggregation: resolution.reducingFunction,
|
|
19781
|
+
rtIds: rtIds && rtIds.length > 0 ? rtIds : undefined,
|
|
19782
|
+
fieldFilter
|
|
19783
|
+
}).then(rows => source.primary
|
|
19784
|
+
? rows.map(r => this.reshapeResolutionRow(r, label))
|
|
19785
|
+
// The transient row's one value column is named after the reducer (`<field>_min` for MIN …);
|
|
19786
|
+
// re-emit it under the series' value field so no reducer suffix is mistaken for a band.
|
|
19787
|
+
: rows.map(r => ({ ...r, cells: [{ attributePath: source.valueField, value: r.cells.find(c => c.attributePath.toLowerCase() !== 'timestamp')?.value ?? null }] })));
|
|
19788
|
+
if (!source.primary) {
|
|
19789
|
+
// One source = one series: a single call over the whole scope, reduced server-side.
|
|
19790
|
+
return { rows: await downsample(ra.scopeRtIds, source.label), downsampled: true };
|
|
19791
|
+
}
|
|
19792
|
+
const rows = ds.aggregateSeriesByGroup && ra.ckTypeId
|
|
19793
|
+
? await this.loadGroupAggregatedRows(ra.ckTypeId, ra.scopeRtIds, downsample)
|
|
19794
|
+
: await this.loadPerRtIdRows(ra.ckTypeId, ra.scopeRtIds, downsample);
|
|
19795
|
+
return { rows, downsampled: true };
|
|
19796
|
+
}
|
|
19797
|
+
const scope = this.stateService.resolveStreamDataScope(ds.entitySelectorId, ds.queryFamily, ds.ignoreTimeFilter);
|
|
19798
|
+
const timeArgs = prepared.timeArgs;
|
|
19799
|
+
const downsampled = ds.queryFamily === 'streamData' && !!timeArgs?.from && !!timeArgs?.to;
|
|
19800
|
+
const streamDataArgs = downsampled
|
|
19801
|
+
? { ...timeArgs, rtIds: prepared.rtIds, limit, queryMode: QueryModeDto.DownsamplingDto }
|
|
19802
|
+
: (timeArgs || prepared.rtIds ? { ...timeArgs, rtIds: prepared.rtIds } : undefined);
|
|
19803
|
+
const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
|
|
19804
|
+
fieldFilter,
|
|
19805
|
+
streamDataArgs,
|
|
19806
|
+
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
19807
|
+
}));
|
|
19808
|
+
const rows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
|
|
19809
|
+
return { rows, downsampled };
|
|
19810
|
+
}
|
|
19811
|
+
/** Writes aligned series into the chart signals (multi-source path, AB#7951). */
|
|
19812
|
+
applyAlignedGrid(instants, series) {
|
|
19813
|
+
const mode = this.stateService.timeZoneMode();
|
|
19814
|
+
const days = new Set(instants.map(t => formatInstant(new Date(t), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '?'));
|
|
19815
|
+
const needsTime = days.size < instants.length;
|
|
19816
|
+
const categories = uniqueCategoryLabels(instants, d => needsTime ? this.formatDateTime(d) : this.formatDate(d));
|
|
19817
|
+
const unitMap = new Map();
|
|
19818
|
+
const seriesData = series.map((s, index) => {
|
|
19819
|
+
if (s.unit)
|
|
19820
|
+
unitMap.set(s.name, s.unit);
|
|
19821
|
+
return {
|
|
19822
|
+
name: s.name,
|
|
19823
|
+
data: s.data,
|
|
19824
|
+
unit: s.unit,
|
|
19825
|
+
color: SERIES_PALETTE[index % SERIES_PALETTE.length],
|
|
19826
|
+
stacked: s.stacked,
|
|
19827
|
+
asLine: s.asLine
|
|
19828
|
+
};
|
|
19829
|
+
});
|
|
19830
|
+
this._categories.set(categories);
|
|
19831
|
+
this._seriesData.set(seriesData);
|
|
19832
|
+
// One value axis: series of different queries share it (per-unit axes stay a single-query feature).
|
|
19833
|
+
this._valueAxes.set([]);
|
|
19834
|
+
this._seriesUnitMap.set(unitMap);
|
|
19835
|
+
}
|
|
19836
|
+
/** Short message of a failed source for its hint. */
|
|
19837
|
+
errorMessage(reason) {
|
|
19838
|
+
const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
|
|
19839
|
+
return message ? `Failed to load data (${message})` : 'Failed to load data';
|
|
19840
|
+
}
|
|
17893
19841
|
buildStreamDataArgs() {
|
|
17894
19842
|
const ds = this.config.dataSource;
|
|
17895
19843
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
@@ -17959,10 +19907,17 @@ class LineChartWidgetComponent {
|
|
|
17959
19907
|
lineValue = this.toNumber(cell.value); // _avg (or an unsuffixed value)
|
|
17960
19908
|
}
|
|
17961
19909
|
else {
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
if (
|
|
17965
|
-
|
|
19910
|
+
// Exact cell first, so a null `<field>_integral` never overrides a usable `<field>`.
|
|
19911
|
+
const val = (findCellForField(row.cells, valueField) ?? cell).value;
|
|
19912
|
+
if (val === null || val === undefined) {
|
|
19913
|
+
// No data (e.g. an Integral over an empty bucket, AB#7975): no point, not 0.
|
|
19914
|
+
lineValue = null;
|
|
19915
|
+
}
|
|
19916
|
+
else {
|
|
19917
|
+
lineValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
19918
|
+
if (isNaN(lineValue))
|
|
19919
|
+
lineValue = 0;
|
|
19920
|
+
}
|
|
17966
19921
|
}
|
|
17967
19922
|
}
|
|
17968
19923
|
else if (unitField && matchesAttributePath(path, unitField)) {
|
|
@@ -18102,6 +20057,15 @@ class LineChartWidgetComponent {
|
|
|
18102
20057
|
</span>
|
|
18103
20058
|
}
|
|
18104
20059
|
}
|
|
20060
|
+
@if (sourceHints().length > 0) {
|
|
20061
|
+
<ul class="source-hints" aria-live="polite">
|
|
20062
|
+
@for (hint of sourceHints(); track hint.text) {
|
|
20063
|
+
<li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
|
|
20064
|
+
@if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
|
|
20065
|
+
</li>
|
|
20066
|
+
}
|
|
20067
|
+
</ul>
|
|
20068
|
+
}
|
|
18105
20069
|
@if (showResolutionInfo() && resolutionExplanation(); as msg) {
|
|
18106
20070
|
<div class="resolution-info">
|
|
18107
20071
|
<div class="ri-head">
|
|
@@ -18173,13 +20137,15 @@ class LineChartWidgetComponent {
|
|
|
18173
20137
|
</kendo-chart-series-item>
|
|
18174
20138
|
}
|
|
18175
20139
|
<kendo-chart-series-item
|
|
18176
|
-
[type]="chartType()"
|
|
20140
|
+
[type]="series.asLine ? 'line' : chartType()"
|
|
18177
20141
|
[style]="'smooth'"
|
|
18178
20142
|
[data]="series.data"
|
|
18179
20143
|
[name]="series.name"
|
|
18180
20144
|
[axis]="series.axisName ?? ''"
|
|
18181
20145
|
[color]="series.color"
|
|
18182
|
-
[opacity]="0.7"
|
|
20146
|
+
[opacity]="series.asLine ? 1 : 0.7"
|
|
20147
|
+
[stack]="series.stacked ?? stackSeries()"
|
|
20148
|
+
[missingValues]="missingValues()"
|
|
18183
20149
|
[markers]="{ visible: config.showMarkers ?? false }">
|
|
18184
20150
|
</kendo-chart-series-item>
|
|
18185
20151
|
}
|
|
@@ -18202,9 +20168,16 @@ class LineChartWidgetComponent {
|
|
|
18202
20168
|
<ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
|
|
18203
20169
|
<div class="chart-tooltip">
|
|
18204
20170
|
<strong>{{ category }}</strong>
|
|
18205
|
-
@
|
|
18206
|
-
|
|
18207
|
-
|
|
20171
|
+
@if (isMultiSource()) {
|
|
20172
|
+
<!-- Multi-source (AB#7952): every drawn series, a gap as "–" (Kendo leaves gap points out of points). -->
|
|
20173
|
+
@for (row of tooltipRows(category); track row.name) {
|
|
20174
|
+
<br/>{{ row.name }}: {{ row.text }}
|
|
20175
|
+
}
|
|
20176
|
+
} @else {
|
|
20177
|
+
@for (point of points; track point.series.name) {
|
|
20178
|
+
@if (!isBandSeries(point.series.name)) {
|
|
20179
|
+
<br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
|
|
20180
|
+
}
|
|
18208
20181
|
}
|
|
18209
20182
|
}
|
|
18210
20183
|
</div>
|
|
@@ -18213,7 +20186,7 @@ class LineChartWidgetComponent {
|
|
|
18213
20186
|
</kendo-chart>
|
|
18214
20187
|
}
|
|
18215
20188
|
</div>
|
|
18216
|
-
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
20189
|
+
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
18217
20190
|
}
|
|
18218
20191
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
|
|
18219
20192
|
type: Component,
|
|
@@ -18243,6 +20216,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18243
20216
|
</span>
|
|
18244
20217
|
}
|
|
18245
20218
|
}
|
|
20219
|
+
@if (sourceHints().length > 0) {
|
|
20220
|
+
<ul class="source-hints" aria-live="polite">
|
|
20221
|
+
@for (hint of sourceHints(); track hint.text) {
|
|
20222
|
+
<li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
|
|
20223
|
+
@if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
|
|
20224
|
+
</li>
|
|
20225
|
+
}
|
|
20226
|
+
</ul>
|
|
20227
|
+
}
|
|
18246
20228
|
@if (showResolutionInfo() && resolutionExplanation(); as msg) {
|
|
18247
20229
|
<div class="resolution-info">
|
|
18248
20230
|
<div class="ri-head">
|
|
@@ -18314,13 +20296,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18314
20296
|
</kendo-chart-series-item>
|
|
18315
20297
|
}
|
|
18316
20298
|
<kendo-chart-series-item
|
|
18317
|
-
[type]="chartType()"
|
|
20299
|
+
[type]="series.asLine ? 'line' : chartType()"
|
|
18318
20300
|
[style]="'smooth'"
|
|
18319
20301
|
[data]="series.data"
|
|
18320
20302
|
[name]="series.name"
|
|
18321
20303
|
[axis]="series.axisName ?? ''"
|
|
18322
20304
|
[color]="series.color"
|
|
18323
|
-
[opacity]="0.7"
|
|
20305
|
+
[opacity]="series.asLine ? 1 : 0.7"
|
|
20306
|
+
[stack]="series.stacked ?? stackSeries()"
|
|
20307
|
+
[missingValues]="missingValues()"
|
|
18324
20308
|
[markers]="{ visible: config.showMarkers ?? false }">
|
|
18325
20309
|
</kendo-chart-series-item>
|
|
18326
20310
|
}
|
|
@@ -18343,9 +20327,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18343
20327
|
<ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
|
|
18344
20328
|
<div class="chart-tooltip">
|
|
18345
20329
|
<strong>{{ category }}</strong>
|
|
18346
|
-
@
|
|
18347
|
-
|
|
18348
|
-
|
|
20330
|
+
@if (isMultiSource()) {
|
|
20331
|
+
<!-- Multi-source (AB#7952): every drawn series, a gap as "–" (Kendo leaves gap points out of points). -->
|
|
20332
|
+
@for (row of tooltipRows(category); track row.name) {
|
|
20333
|
+
<br/>{{ row.name }}: {{ row.text }}
|
|
20334
|
+
}
|
|
20335
|
+
} @else {
|
|
20336
|
+
@for (point of points; track point.series.name) {
|
|
20337
|
+
@if (!isBandSeries(point.series.name)) {
|
|
20338
|
+
<br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
|
|
20339
|
+
}
|
|
18349
20340
|
}
|
|
18350
20341
|
}
|
|
18351
20342
|
</div>
|
|
@@ -18354,7 +20345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18354
20345
|
</kendo-chart>
|
|
18355
20346
|
}
|
|
18356
20347
|
</div>
|
|
18357
|
-
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
|
|
20348
|
+
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
|
|
18358
20349
|
}], propDecorators: { config: [{
|
|
18359
20350
|
type: Input
|
|
18360
20351
|
}] } });
|
|
@@ -18368,6 +20359,7 @@ class LineChartConfigDialogComponent {
|
|
|
18368
20359
|
queryExecutor = inject(QueryExecutorService);
|
|
18369
20360
|
stateService = inject(MeshBoardStateService);
|
|
18370
20361
|
windowRef = inject(WindowRef);
|
|
20362
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
18371
20363
|
querySelector;
|
|
18372
20364
|
// Initial values for editing
|
|
18373
20365
|
initialQueryRtId;
|
|
@@ -18388,6 +20380,13 @@ class LineChartConfigDialogComponent {
|
|
|
18388
20380
|
initialShowDataBadge;
|
|
18389
20381
|
initialReferenceLines;
|
|
18390
20382
|
initialFilters;
|
|
20383
|
+
initialAdditionalSeries;
|
|
20384
|
+
initialStacked;
|
|
20385
|
+
initialPrimarySeries;
|
|
20386
|
+
/** Query and formula series of the Series section (AB#7951, AB#7952). */
|
|
20387
|
+
additionalSeries = [];
|
|
20388
|
+
/** Display options of the primary query's series (AB#7952). */
|
|
20389
|
+
primarySeries;
|
|
18391
20390
|
// State
|
|
18392
20391
|
isLoadingInitial = false;
|
|
18393
20392
|
isLoadingColumns = false;
|
|
@@ -18442,13 +20441,19 @@ class LineChartConfigDialogComponent {
|
|
|
18442
20441
|
legendPosition: 'right',
|
|
18443
20442
|
showMarkers: false,
|
|
18444
20443
|
showDataBadge: true,
|
|
20444
|
+
stacked: false,
|
|
18445
20445
|
referenceLines: []
|
|
18446
20446
|
};
|
|
18447
20447
|
get isValid() {
|
|
18448
20448
|
return this.selectedPersistentQuery !== null
|
|
18449
20449
|
&& this.form.categoryField !== ''
|
|
18450
20450
|
&& this.form.seriesGroupField !== ''
|
|
18451
|
-
&& this.form.valueField !== ''
|
|
20451
|
+
&& this.form.valueField !== ''
|
|
20452
|
+
&& querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator).every(e => e === null);
|
|
20453
|
+
}
|
|
20454
|
+
/** The query series that are saved: none unless the primary query is a stream-data query. */
|
|
20455
|
+
get effectiveAdditionalSeries() {
|
|
20456
|
+
return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
|
|
18452
20457
|
}
|
|
18453
20458
|
async ngOnInit() {
|
|
18454
20459
|
// Initialize filter variables from MeshBoard state
|
|
@@ -18472,6 +20477,9 @@ class LineChartConfigDialogComponent {
|
|
|
18472
20477
|
this.form.showMarkers = this.initialShowMarkers ?? false;
|
|
18473
20478
|
this.form.showDataBadge = this.initialShowDataBadge ?? true;
|
|
18474
20479
|
this.form.referenceLines = this.initialReferenceLines ? [...this.initialReferenceLines] : [];
|
|
20480
|
+
this.form.stacked = this.initialStacked ?? false;
|
|
20481
|
+
this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
|
|
20482
|
+
this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
|
|
18475
20483
|
// Initialize filters
|
|
18476
20484
|
if (this.initialFilters && this.initialFilters.length > 0) {
|
|
18477
20485
|
this.filters = this.initialFilters.map((f, index) => ({
|
|
@@ -18553,8 +20561,10 @@ class LineChartConfigDialogComponent {
|
|
|
18553
20561
|
aggregationType: c.aggregationType ?? null
|
|
18554
20562
|
}));
|
|
18555
20563
|
}
|
|
18556
|
-
|
|
18557
|
-
|
|
20564
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
20565
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
20566
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
|
|
20567
|
+
return columns.map(c => ({
|
|
18558
20568
|
attributePath: c.attributePath,
|
|
18559
20569
|
attributeValueType: c.attributeValueType ?? '',
|
|
18560
20570
|
aggregationType: c.aggregationType ?? null
|
|
@@ -18594,7 +20604,10 @@ class LineChartConfigDialogComponent {
|
|
|
18594
20604
|
showMarkers: this.form.showMarkers,
|
|
18595
20605
|
showDataBadge: this.form.showDataBadge,
|
|
18596
20606
|
referenceLines: this.form.referenceLines.length > 0 ? this.form.referenceLines : undefined,
|
|
18597
|
-
filters: filtersDto
|
|
20607
|
+
filters: filtersDto,
|
|
20608
|
+
additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
|
|
20609
|
+
stacked: this.form.chartType === 'area' && this.form.stacked ? true : undefined,
|
|
20610
|
+
primarySeries: this.selectedQueryFamily === 'streamData' ? this.primarySeries : undefined
|
|
18598
20611
|
};
|
|
18599
20612
|
this.windowRef.close(result);
|
|
18600
20613
|
}
|
|
@@ -18608,7 +20621,7 @@ class LineChartConfigDialogComponent {
|
|
|
18608
20621
|
this.windowRef.close();
|
|
18609
20622
|
}
|
|
18610
20623
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18611
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
20624
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialStacked: "initialStacked", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
|
|
18612
20625
|
<div class="config-container">
|
|
18613
20626
|
|
|
18614
20627
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -18749,6 +20762,22 @@ class LineChartConfigDialogComponent {
|
|
|
18749
20762
|
</div>
|
|
18750
20763
|
}
|
|
18751
20764
|
|
|
20765
|
+
<!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
|
|
20766
|
+
@if (selectedQueryFamily === 'streamData') {
|
|
20767
|
+
<div class="config-section">
|
|
20768
|
+
<h3 class="section-title">Series</h3>
|
|
20769
|
+
<p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
|
|
20770
|
+
<mm-line-chart-series-editor
|
|
20771
|
+
[series]="additionalSeries"
|
|
20772
|
+
[selectors]="availableEntitySelectors"
|
|
20773
|
+
[primary]="primarySeries"
|
|
20774
|
+
[stackedChart]="form.chartType === 'area' && form.stacked"
|
|
20775
|
+
(seriesChange)="additionalSeries = $event"
|
|
20776
|
+
(primaryChange)="primarySeries = $event">
|
|
20777
|
+
</mm-line-chart-series-editor>
|
|
20778
|
+
</div>
|
|
20779
|
+
}
|
|
20780
|
+
|
|
18752
20781
|
<!-- Filters Section -->
|
|
18753
20782
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
18754
20783
|
<div class="config-section">
|
|
@@ -18790,6 +20819,16 @@ class LineChartConfigDialogComponent {
|
|
|
18790
20819
|
</div>
|
|
18791
20820
|
</div>
|
|
18792
20821
|
|
|
20822
|
+
@if (form.chartType === 'area') {
|
|
20823
|
+
<div class="form-field checkbox-field">
|
|
20824
|
+
<label>
|
|
20825
|
+
<input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
|
|
20826
|
+
Stacked
|
|
20827
|
+
</label>
|
|
20828
|
+
</div>
|
|
20829
|
+
<p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
|
|
20830
|
+
}
|
|
20831
|
+
|
|
18793
20832
|
<div class="form-row">
|
|
18794
20833
|
<div class="form-field checkbox-field">
|
|
18795
20834
|
<label>
|
|
@@ -18853,7 +20892,7 @@ class LineChartConfigDialogComponent {
|
|
|
18853
20892
|
</button>
|
|
18854
20893
|
</div>
|
|
18855
20894
|
</div>
|
|
18856
|
-
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
20895
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
18857
20896
|
}
|
|
18858
20897
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
|
|
18859
20898
|
type: Component,
|
|
@@ -18868,7 +20907,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18868
20907
|
QuerySelectorComponent,
|
|
18869
20908
|
SdTimeFilterToggleComponent,
|
|
18870
20909
|
EntitySelectorScopePickerComponent,
|
|
18871
|
-
LoadingOverlayComponent
|
|
20910
|
+
LoadingOverlayComponent,
|
|
20911
|
+
LineChartSeriesEditorComponent
|
|
18872
20912
|
], template: `
|
|
18873
20913
|
<div class="config-container">
|
|
18874
20914
|
|
|
@@ -19010,6 +21050,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19010
21050
|
</div>
|
|
19011
21051
|
}
|
|
19012
21052
|
|
|
21053
|
+
<!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
|
|
21054
|
+
@if (selectedQueryFamily === 'streamData') {
|
|
21055
|
+
<div class="config-section">
|
|
21056
|
+
<h3 class="section-title">Series</h3>
|
|
21057
|
+
<p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
|
|
21058
|
+
<mm-line-chart-series-editor
|
|
21059
|
+
[series]="additionalSeries"
|
|
21060
|
+
[selectors]="availableEntitySelectors"
|
|
21061
|
+
[primary]="primarySeries"
|
|
21062
|
+
[stackedChart]="form.chartType === 'area' && form.stacked"
|
|
21063
|
+
(seriesChange)="additionalSeries = $event"
|
|
21064
|
+
(primaryChange)="primarySeries = $event">
|
|
21065
|
+
</mm-line-chart-series-editor>
|
|
21066
|
+
</div>
|
|
21067
|
+
}
|
|
21068
|
+
|
|
19013
21069
|
<!-- Filters Section -->
|
|
19014
21070
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
19015
21071
|
<div class="config-section">
|
|
@@ -19051,6 +21107,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19051
21107
|
</div>
|
|
19052
21108
|
</div>
|
|
19053
21109
|
|
|
21110
|
+
@if (form.chartType === 'area') {
|
|
21111
|
+
<div class="form-field checkbox-field">
|
|
21112
|
+
<label>
|
|
21113
|
+
<input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
|
|
21114
|
+
Stacked
|
|
21115
|
+
</label>
|
|
21116
|
+
</div>
|
|
21117
|
+
<p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
|
|
21118
|
+
}
|
|
21119
|
+
|
|
19054
21120
|
<div class="form-row">
|
|
19055
21121
|
<div class="form-field checkbox-field">
|
|
19056
21122
|
<label>
|
|
@@ -19154,6 +21220,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19154
21220
|
type: Input
|
|
19155
21221
|
}], initialFilters: [{
|
|
19156
21222
|
type: Input
|
|
21223
|
+
}], initialAdditionalSeries: [{
|
|
21224
|
+
type: Input
|
|
21225
|
+
}], initialStacked: [{
|
|
21226
|
+
type: Input
|
|
21227
|
+
}], initialPrimarySeries: [{
|
|
21228
|
+
type: Input
|
|
19157
21229
|
}] } });
|
|
19158
21230
|
|
|
19159
21231
|
/**
|
|
@@ -19520,6 +21592,22 @@ class HeatmapWidgetComponent {
|
|
|
19520
21592
|
numericValue = 0;
|
|
19521
21593
|
}
|
|
19522
21594
|
}
|
|
21595
|
+
// A null value cell is "no data" (e.g. an Integral over an empty window, AB#7975): the
|
|
21596
|
+
// row is skipped instead of counting as 0 in its bucket (sum/avg). The value cell is
|
|
21597
|
+
// resolved exactly (`findCellForField`), so a null `<field>_integral` never hides or
|
|
21598
|
+
// overwrites a usable `<field>`.
|
|
21599
|
+
// A count ignores the value; a weighted mean validates value and weight below.
|
|
21600
|
+
if (valueField && aggregation !== 'count' && !weighted) {
|
|
21601
|
+
const valueCell = findCellForField(row.cells, valueField);
|
|
21602
|
+
if (valueCell) {
|
|
21603
|
+
const val = valueCell.value;
|
|
21604
|
+
if (val === null || val === undefined)
|
|
21605
|
+
continue;
|
|
21606
|
+
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
21607
|
+
if (isNaN(numericValue))
|
|
21608
|
+
numericValue = 0;
|
|
21609
|
+
}
|
|
21610
|
+
}
|
|
19523
21611
|
// Stream-data rows carry their time at top level (`row.timestamp`, see
|
|
19524
21612
|
// QueryExecutorService.mapStreamDataRows); the cells only hold the value
|
|
19525
21613
|
// columns. Same source the line chart uses for the bin time (AB#7938).
|
|
@@ -20152,8 +22240,10 @@ class HeatmapConfigDialogComponent {
|
|
|
20152
22240
|
aggregationType: c.aggregationType ?? null
|
|
20153
22241
|
}));
|
|
20154
22242
|
}
|
|
20155
|
-
|
|
20156
|
-
|
|
22243
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
22244
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
22245
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
|
|
22246
|
+
return columns.map(c => ({
|
|
20157
22247
|
attributePath: c.attributePath,
|
|
20158
22248
|
attributeValueType: c.attributeValueType ?? '',
|
|
20159
22249
|
aggregationType: c.aggregationType ?? null
|
|
@@ -23180,8 +25270,10 @@ class WidgetGroupConfigDialogComponent {
|
|
|
23180
25270
|
aggregationType: c.aggregationType ?? null
|
|
23181
25271
|
}));
|
|
23182
25272
|
}
|
|
23183
|
-
|
|
23184
|
-
|
|
25273
|
+
// An Integral column needs from/to (AB#7921): the executor retries with the board's
|
|
25274
|
+
// window, or the last 24 hours when the board has no time filter (AB#7975).
|
|
25275
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
25276
|
+
return columns.map(c => ({
|
|
23185
25277
|
attributePath: c.attributePath,
|
|
23186
25278
|
attributeValueType: c.attributeValueType ?? '',
|
|
23187
25279
|
aggregationType: c.aggregationType ?? null
|
|
@@ -26023,6 +28115,332 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
26023
28115
|
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [".config-form{display:flex;flex-direction:column;gap:12px;padding:16px}.form-group{display:flex;flex-direction:column;gap:4px}.form-row{display:flex;gap:16px}.form-row .form-group{flex:1}.hint{font-size:.75rem;opacity:.6}\n"] }]
|
|
26024
28116
|
}] });
|
|
26025
28117
|
|
|
28118
|
+
/**
|
|
28119
|
+
* Process Widget Type identifier
|
|
28120
|
+
*/
|
|
28121
|
+
const PROCESS_WIDGET_TYPE = 'process';
|
|
28122
|
+
/**
|
|
28123
|
+
* Name of the implicit binding a config without `bindings` is read as: the
|
|
28124
|
+
* legacy single-binding fields (`dataBindingMode`, `bindingCkTypeId`, …).
|
|
28125
|
+
* Mappings without `bindingName` refer to it.
|
|
28126
|
+
*/
|
|
28127
|
+
const DEFAULT_PROCESS_BINDING_NAME = 'default';
|
|
28128
|
+
/**
|
|
28129
|
+
* Default configuration for a new Process Widget
|
|
28130
|
+
*/
|
|
28131
|
+
const DEFAULT_PROCESS_WIDGET_CONFIG = {
|
|
28132
|
+
type: 'process',
|
|
28133
|
+
fitToBounds: true,
|
|
28134
|
+
allowZoom: false,
|
|
28135
|
+
allowPan: false,
|
|
28136
|
+
showToolbar: false,
|
|
28137
|
+
initialZoom: 1
|
|
28138
|
+
};
|
|
28139
|
+
|
|
28140
|
+
/**
|
|
28141
|
+
* Aggregation of a mapped Process Widget value over all rows of a query
|
|
28142
|
+
* binding's selector scope (AB#8024): `first` (default, the first row as
|
|
28143
|
+
* before), `sum`, `avg`, `min`, `max`.
|
|
28144
|
+
*
|
|
28145
|
+
* Aggregation works on **query bindings only**: a runtime-entity binding loads
|
|
28146
|
+
* a single entity, so there is nothing to aggregate. To aggregate over all
|
|
28147
|
+
* entities of a scope, bind a runtime persistent query with the entity
|
|
28148
|
+
* selector instead.
|
|
28149
|
+
*/
|
|
28150
|
+
/** All aggregates a mapping may name. */
|
|
28151
|
+
const PROCESS_VALUE_AGGREGATES = ['first', 'sum', 'avg', 'min', 'max'];
|
|
28152
|
+
/** `true` when the aggregate reduces several rows (anything but `first`/absent). */
|
|
28153
|
+
function isReducingAggregate(aggregate) {
|
|
28154
|
+
return aggregate !== undefined && aggregate !== 'first';
|
|
28155
|
+
}
|
|
28156
|
+
/**
|
|
28157
|
+
* Reads one cell of a query row: the column name, or its sanitized form
|
|
28158
|
+
* (dots → underscores) as the query service delivers nested paths.
|
|
28159
|
+
*/
|
|
28160
|
+
function cellValue(cells, columnName) {
|
|
28161
|
+
return cells.get(columnName) ?? cells.get(columnName.replace(/\./g, '_'));
|
|
28162
|
+
}
|
|
28163
|
+
/** A finite number from a cell, or `undefined` for null/undefined/non-numeric cells. */
|
|
28164
|
+
function numericCell(value) {
|
|
28165
|
+
if (typeof value === 'number') {
|
|
28166
|
+
return Number.isFinite(value) ? value : undefined;
|
|
28167
|
+
}
|
|
28168
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
28169
|
+
const parsed = Number(value);
|
|
28170
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
28171
|
+
}
|
|
28172
|
+
return undefined;
|
|
28173
|
+
}
|
|
28174
|
+
/**
|
|
28175
|
+
* The value of a column over the query rows.
|
|
28176
|
+
*
|
|
28177
|
+
* - `first` / absent: the cell of the first row, unchanged (today's behaviour).
|
|
28178
|
+
* - `sum`, `avg`, `min`, `max`: over the numeric cells; null, undefined and
|
|
28179
|
+
* non-numeric cells are skipped. Zero rows or no numeric cell ⇒ `undefined`
|
|
28180
|
+
* (the property keeps no value, not 0).
|
|
28181
|
+
*/
|
|
28182
|
+
function aggregateQueryColumn(queryResult, columnName, aggregate) {
|
|
28183
|
+
const rows = queryResult.rows ?? [];
|
|
28184
|
+
if (rows.length === 0) {
|
|
28185
|
+
return undefined;
|
|
28186
|
+
}
|
|
28187
|
+
if (!isReducingAggregate(aggregate)) {
|
|
28188
|
+
return cellValue(rows[0].cells, columnName);
|
|
28189
|
+
}
|
|
28190
|
+
const numbers = [];
|
|
28191
|
+
for (const row of rows) {
|
|
28192
|
+
const n = numericCell(cellValue(row.cells, columnName));
|
|
28193
|
+
if (n !== undefined) {
|
|
28194
|
+
numbers.push(n);
|
|
28195
|
+
}
|
|
28196
|
+
}
|
|
28197
|
+
if (numbers.length === 0) {
|
|
28198
|
+
return undefined;
|
|
28199
|
+
}
|
|
28200
|
+
switch (aggregate) {
|
|
28201
|
+
case 'sum':
|
|
28202
|
+
return numbers.reduce((a, b) => a + b, 0);
|
|
28203
|
+
case 'avg':
|
|
28204
|
+
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
28205
|
+
case 'min':
|
|
28206
|
+
return Math.min(...numbers);
|
|
28207
|
+
case 'max':
|
|
28208
|
+
return Math.max(...numbers);
|
|
28209
|
+
default:
|
|
28210
|
+
return undefined;
|
|
28211
|
+
}
|
|
28212
|
+
}
|
|
28213
|
+
/**
|
|
28214
|
+
* `true` when a reducing aggregate ran over fewer rows than the scope holds
|
|
28215
|
+
* (`totalCount` > loaded rows) — the result is partial and worth a warning.
|
|
28216
|
+
*/
|
|
28217
|
+
function isPartialAggregate(queryResult) {
|
|
28218
|
+
const loaded = queryResult.rows?.length ?? 0;
|
|
28219
|
+
return typeof queryResult.totalCount === 'number' && queryResult.totalCount > loaded;
|
|
28220
|
+
}
|
|
28221
|
+
/**
|
|
28222
|
+
* Why a mapping's aggregate cannot apply, or `undefined` when it can (or is
|
|
28223
|
+
* `first`/absent). A reducing aggregate needs a `number` property and a
|
|
28224
|
+
* persistent-query binding. The widget falls back to `first` in those cases.
|
|
28225
|
+
*
|
|
28226
|
+
* @param propertyType Type of the diagram's transform property, when known.
|
|
28227
|
+
* @param bindingMode Mode of the mapping's binding, when known.
|
|
28228
|
+
*/
|
|
28229
|
+
function processAggregateIssue(aggregate, propertyType, bindingMode) {
|
|
28230
|
+
if (aggregate === undefined) {
|
|
28231
|
+
return undefined;
|
|
28232
|
+
}
|
|
28233
|
+
if (!PROCESS_VALUE_AGGREGATES.includes(aggregate)) {
|
|
28234
|
+
return `Unknown aggregate '${String(aggregate)}'`;
|
|
28235
|
+
}
|
|
28236
|
+
if (!isReducingAggregate(aggregate)) {
|
|
28237
|
+
return undefined;
|
|
28238
|
+
}
|
|
28239
|
+
if (bindingMode === 'runtimeEntity') {
|
|
28240
|
+
return `Aggregate '${aggregate}' needs a persistent query binding; a runtime entity binding loads one entity`;
|
|
28241
|
+
}
|
|
28242
|
+
if (propertyType !== undefined && propertyType !== 'number') {
|
|
28243
|
+
return `Aggregate '${aggregate}' needs a number property, not '${propertyType}'`;
|
|
28244
|
+
}
|
|
28245
|
+
return undefined;
|
|
28246
|
+
}
|
|
28247
|
+
|
|
28248
|
+
/**
|
|
28249
|
+
* The legacy single-binding fields read as one binding named
|
|
28250
|
+
* {@link DEFAULT_PROCESS_BINDING_NAME}, or `undefined` when the legacy mode is
|
|
28251
|
+
* absent or `'none'`.
|
|
28252
|
+
*/
|
|
28253
|
+
function legacyProcessBinding(config) {
|
|
28254
|
+
const mode = config.dataBindingMode;
|
|
28255
|
+
if (!mode || mode === 'none') {
|
|
28256
|
+
return undefined;
|
|
28257
|
+
}
|
|
28258
|
+
return {
|
|
28259
|
+
name: DEFAULT_PROCESS_BINDING_NAME,
|
|
28260
|
+
mode,
|
|
28261
|
+
ckTypeId: config.bindingCkTypeId,
|
|
28262
|
+
rtId: config.bindingRtId,
|
|
28263
|
+
queryRtId: config.bindingQueryRtId,
|
|
28264
|
+
queryName: config.bindingQueryName,
|
|
28265
|
+
queryFamily: config.bindingQueryFamily,
|
|
28266
|
+
ignoreTimeFilter: config.bindingIgnoreTimeFilter,
|
|
28267
|
+
entitySelectorId: config.bindingEntitySelectorId,
|
|
28268
|
+
filters: config.bindingFilters
|
|
28269
|
+
};
|
|
28270
|
+
}
|
|
28271
|
+
/** `true` when the config uses the named `bindings` list instead of the legacy fields. */
|
|
28272
|
+
function hasNamedProcessBindings(config) {
|
|
28273
|
+
return Array.isArray(config.bindings) && config.bindings.length > 0;
|
|
28274
|
+
}
|
|
28275
|
+
/**
|
|
28276
|
+
* The bindings the widget loads: the named `bindings` when present, otherwise
|
|
28277
|
+
* the implicit legacy binding (or none).
|
|
28278
|
+
*/
|
|
28279
|
+
function resolveProcessBindings(config) {
|
|
28280
|
+
if (hasNamedProcessBindings(config)) {
|
|
28281
|
+
return config.bindings;
|
|
28282
|
+
}
|
|
28283
|
+
const legacy = legacyProcessBinding(config);
|
|
28284
|
+
return legacy ? [legacy] : [];
|
|
28285
|
+
}
|
|
28286
|
+
/** The binding name a mapping reads from (`default` when absent). */
|
|
28287
|
+
function mappingBindingName(mapping) {
|
|
28288
|
+
return mapping.bindingName || DEFAULT_PROCESS_BINDING_NAME;
|
|
28289
|
+
}
|
|
28290
|
+
/**
|
|
28291
|
+
* The config problem of a single binding, or `undefined` when it can load.
|
|
28292
|
+
* A runtime-entity binding needs a CK type (or a selector to take it from) and
|
|
28293
|
+
* an rtId or an entity selector; a query binding needs a query rtId.
|
|
28294
|
+
*/
|
|
28295
|
+
function processBindingConfigError(binding) {
|
|
28296
|
+
if (binding.mode === 'runtimeEntity') {
|
|
28297
|
+
if (!binding.rtId && !binding.entitySelectorId) {
|
|
28298
|
+
return 'Runtime entity binding needs an rtId or an entity selector';
|
|
28299
|
+
}
|
|
28300
|
+
if (!binding.ckTypeId && !binding.entitySelectorId) {
|
|
28301
|
+
return 'Runtime entity binding needs a CK type';
|
|
28302
|
+
}
|
|
28303
|
+
return undefined;
|
|
28304
|
+
}
|
|
28305
|
+
if (binding.mode === 'persistentQuery') {
|
|
28306
|
+
return binding.queryRtId ? undefined : 'Persistent query binding needs a query rtId';
|
|
28307
|
+
}
|
|
28308
|
+
return `Unknown binding mode '${String(binding.mode)}'`;
|
|
28309
|
+
}
|
|
28310
|
+
/**
|
|
28311
|
+
* Validates the bindings and mappings of a Process Widget config (AB#8023):
|
|
28312
|
+
* empty or duplicate binding names, bindings that cannot load (see
|
|
28313
|
+
* {@link processBindingConfigError}), selectors that do not exist on the board
|
|
28314
|
+
* (only checked when `knownSelectorIds` is given), and mappings that refer to an
|
|
28315
|
+
* unknown binding. A legacy config is validated like its implicit binding; a
|
|
28316
|
+
* config without any binding has no issues.
|
|
28317
|
+
*
|
|
28318
|
+
* Aggregates (AB#8024): an unknown aggregate, or one other than `first` on a
|
|
28319
|
+
* runtime-entity binding, an `attribute` mapping or — when `propertyTypes` is
|
|
28320
|
+
* given — a non-`number` property, is reported; the widget then reads the
|
|
28321
|
+
* first row.
|
|
28322
|
+
*
|
|
28323
|
+
* @param propertyTypes Type per diagram property id (`transformProperties`), when the diagram is known.
|
|
28324
|
+
*/
|
|
28325
|
+
function validateProcessWidgetBindings(config, knownSelectorIds, propertyTypes) {
|
|
28326
|
+
const issues = [];
|
|
28327
|
+
const bindings = resolveProcessBindings(config);
|
|
28328
|
+
const names = new Set();
|
|
28329
|
+
for (const binding of bindings) {
|
|
28330
|
+
if (!binding.name || !binding.name.trim()) {
|
|
28331
|
+
issues.push({ message: 'A binding has no name' });
|
|
28332
|
+
continue;
|
|
28333
|
+
}
|
|
28334
|
+
if (names.has(binding.name)) {
|
|
28335
|
+
issues.push({ bindingName: binding.name, message: `Duplicate binding name '${binding.name}'` });
|
|
28336
|
+
}
|
|
28337
|
+
names.add(binding.name);
|
|
28338
|
+
const error = processBindingConfigError(binding);
|
|
28339
|
+
if (error) {
|
|
28340
|
+
issues.push({ bindingName: binding.name, message: error });
|
|
28341
|
+
}
|
|
28342
|
+
if (binding.entitySelectorId && knownSelectorIds && !knownSelectorIds.includes(binding.entitySelectorId)) {
|
|
28343
|
+
issues.push({
|
|
28344
|
+
bindingName: binding.name,
|
|
28345
|
+
message: `Entity selector '${binding.entitySelectorId}' does not exist on the board`
|
|
28346
|
+
});
|
|
28347
|
+
}
|
|
28348
|
+
}
|
|
28349
|
+
// Only report unknown names when the widget has bindings at all — a widget
|
|
28350
|
+
// without data binding keeps stale mappings silently, as before.
|
|
28351
|
+
if (bindings.length > 0) {
|
|
28352
|
+
for (const mapping of config.propertyMappings ?? []) {
|
|
28353
|
+
const name = mappingBindingName(mapping);
|
|
28354
|
+
if (!names.has(name)) {
|
|
28355
|
+
issues.push({
|
|
28356
|
+
bindingName: name,
|
|
28357
|
+
propertyId: mapping.propertyId,
|
|
28358
|
+
message: `Mapping of '${mapping.propertyId}' refers to unknown binding '${name}'`
|
|
28359
|
+
});
|
|
28360
|
+
}
|
|
28361
|
+
const aggregateIssue = mappingAggregateIssue(mapping, bindings.find(b => b.name === name), propertyTypes);
|
|
28362
|
+
if (aggregateIssue) {
|
|
28363
|
+
issues.push({
|
|
28364
|
+
bindingName: name,
|
|
28365
|
+
propertyId: mapping.propertyId,
|
|
28366
|
+
message: `Mapping of '${mapping.propertyId}': ${aggregateIssue}; the first row is used`
|
|
28367
|
+
});
|
|
28368
|
+
}
|
|
28369
|
+
}
|
|
28370
|
+
}
|
|
28371
|
+
return issues;
|
|
28372
|
+
}
|
|
28373
|
+
/**
|
|
28374
|
+
* Why a mapping's `aggregate` cannot apply (AB#8024), or `undefined`. Besides
|
|
28375
|
+
* {@link processAggregateIssue} an `attribute` mapping cannot aggregate: it
|
|
28376
|
+
* reads one entity.
|
|
28377
|
+
*/
|
|
28378
|
+
function mappingAggregateIssue(mapping, binding, propertyTypes) {
|
|
28379
|
+
const issue = processAggregateIssue(mapping.aggregate, propertyTypes?.get(mapping.propertyId), binding?.mode);
|
|
28380
|
+
if (issue) {
|
|
28381
|
+
return issue;
|
|
28382
|
+
}
|
|
28383
|
+
if (mapping.aggregate && mapping.aggregate !== 'first' && mapping.sourceType === 'attribute') {
|
|
28384
|
+
return `Aggregate '${mapping.aggregate}' needs a column mapping; an attribute mapping reads one entity`;
|
|
28385
|
+
}
|
|
28386
|
+
return undefined;
|
|
28387
|
+
}
|
|
28388
|
+
/**
|
|
28389
|
+
* Reads persisted `bindings` (JSON/YAML seed) into typed bindings. Entries that
|
|
28390
|
+
* are not objects or have an unknown mode are dropped; returns `undefined` when
|
|
28391
|
+
* nothing usable is left, so a config without bindings stays without the key.
|
|
28392
|
+
*/
|
|
28393
|
+
function parsePersistedProcessBindings(raw) {
|
|
28394
|
+
if (!Array.isArray(raw)) {
|
|
28395
|
+
return undefined;
|
|
28396
|
+
}
|
|
28397
|
+
const bindings = [];
|
|
28398
|
+
for (const entry of raw) {
|
|
28399
|
+
if (!entry || typeof entry !== 'object') {
|
|
28400
|
+
continue;
|
|
28401
|
+
}
|
|
28402
|
+
const e = entry;
|
|
28403
|
+
if (e['mode'] !== 'runtimeEntity' && e['mode'] !== 'persistentQuery') {
|
|
28404
|
+
console.warn('[ProcessWidget] Ignoring binding with unknown mode:', e['name'], e['mode']);
|
|
28405
|
+
continue;
|
|
28406
|
+
}
|
|
28407
|
+
bindings.push(compactBinding({
|
|
28408
|
+
name: typeof e['name'] === 'string' ? e['name'] : '',
|
|
28409
|
+
mode: e['mode'],
|
|
28410
|
+
ckTypeId: e['ckTypeId'],
|
|
28411
|
+
rtId: e['rtId'],
|
|
28412
|
+
queryRtId: e['queryRtId'],
|
|
28413
|
+
queryName: e['queryName'],
|
|
28414
|
+
queryFamily: e['queryFamily'],
|
|
28415
|
+
ignoreTimeFilter: e['ignoreTimeFilter'],
|
|
28416
|
+
entitySelectorId: e['entitySelectorId'],
|
|
28417
|
+
filters: e['filters']
|
|
28418
|
+
}));
|
|
28419
|
+
}
|
|
28420
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
28421
|
+
}
|
|
28422
|
+
/**
|
|
28423
|
+
* Serializes bindings for persistence: only set keys are written
|
|
28424
|
+
* (`ignoreTimeFilter` only when `true`, `filters` only when non-empty).
|
|
28425
|
+
*/
|
|
28426
|
+
function toPersistedProcessBindings(bindings) {
|
|
28427
|
+
return bindings.map(compactBinding);
|
|
28428
|
+
}
|
|
28429
|
+
function compactBinding(binding) {
|
|
28430
|
+
return {
|
|
28431
|
+
name: binding.name,
|
|
28432
|
+
mode: binding.mode,
|
|
28433
|
+
...(binding.ckTypeId && { ckTypeId: binding.ckTypeId }),
|
|
28434
|
+
...(binding.rtId && { rtId: binding.rtId }),
|
|
28435
|
+
...(binding.queryRtId && { queryRtId: binding.queryRtId }),
|
|
28436
|
+
...(binding.queryName && { queryName: binding.queryName }),
|
|
28437
|
+
...(binding.queryFamily && { queryFamily: binding.queryFamily }),
|
|
28438
|
+
...(binding.ignoreTimeFilter === true && { ignoreTimeFilter: true }),
|
|
28439
|
+
...(binding.entitySelectorId && { entitySelectorId: binding.entitySelectorId }),
|
|
28440
|
+
...(binding.filters && binding.filters.length > 0 && { filters: binding.filters })
|
|
28441
|
+
};
|
|
28442
|
+
}
|
|
28443
|
+
|
|
26026
28444
|
class ProcessDataService {
|
|
26027
28445
|
dataService = inject(MeshBoardDataService);
|
|
26028
28446
|
variableService = inject(MeshBoardVariableService);
|
|
@@ -26488,51 +28906,90 @@ class ProcessDataService {
|
|
|
26488
28906
|
// Widget Data Binding Methods
|
|
26489
28907
|
// ============================================================================
|
|
26490
28908
|
/**
|
|
26491
|
-
* Loads
|
|
26492
|
-
*
|
|
28909
|
+
* Loads data based on the widget's legacy single-binding configuration —
|
|
28910
|
+
* the implicit binding named `default` (see {@link legacyProcessBinding}).
|
|
26493
28911
|
*
|
|
26494
28912
|
* @param config - The process widget configuration with data binding settings
|
|
26495
28913
|
* @returns BoundDataResult containing the loaded data, or null if no binding configured
|
|
26496
28914
|
*/
|
|
26497
28915
|
async loadBoundData(config) {
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
28916
|
+
const binding = legacyProcessBinding(config);
|
|
28917
|
+
return binding ? this.loadBinding(binding) : null;
|
|
28918
|
+
}
|
|
28919
|
+
/**
|
|
28920
|
+
* Loads every binding of the widget in parallel (AB#8023): the named
|
|
28921
|
+
* `bindings`, or the implicit legacy binding `default`. One result per
|
|
28922
|
+
* binding name (first binding wins on a duplicate name). A failing binding
|
|
28923
|
+
* yields a result with `error`, a binding whose selector scope is still
|
|
28924
|
+
* resolving a result with `pending` — neither blocks the others.
|
|
28925
|
+
*/
|
|
28926
|
+
async loadBindings(config) {
|
|
28927
|
+
const bindings = resolveProcessBindings(config);
|
|
28928
|
+
const results = await Promise.all(bindings.map(binding => this.loadBinding(binding)));
|
|
28929
|
+
const byName = new Map();
|
|
28930
|
+
bindings.forEach((binding, index) => {
|
|
28931
|
+
if (!byName.has(binding.name)) {
|
|
28932
|
+
byName.set(binding.name, results[index]);
|
|
26504
28933
|
}
|
|
26505
|
-
|
|
26506
|
-
|
|
28934
|
+
});
|
|
28935
|
+
return byName;
|
|
28936
|
+
}
|
|
28937
|
+
/**
|
|
28938
|
+
* Loads one binding. Never throws: errors are returned as `error` result.
|
|
28939
|
+
*/
|
|
28940
|
+
async loadBinding(binding) {
|
|
28941
|
+
const type = binding.mode === 'runtimeEntity' ? 'entity' : 'query';
|
|
28942
|
+
try {
|
|
28943
|
+
const configError = processBindingConfigError(binding);
|
|
28944
|
+
if (configError) {
|
|
28945
|
+
throw new Error(configError);
|
|
26507
28946
|
}
|
|
26508
|
-
|
|
28947
|
+
const result = binding.mode === 'runtimeEntity'
|
|
28948
|
+
? await this.loadBoundRuntimeEntityData(binding)
|
|
28949
|
+
: await this.loadBoundQueryData(binding);
|
|
28950
|
+
return { ...result, bindingName: binding.name };
|
|
26509
28951
|
}
|
|
26510
28952
|
catch (error) {
|
|
26511
|
-
console.error(
|
|
28953
|
+
console.error(`[ProcessDataService] Error loading binding '${binding.name}':`, error);
|
|
26512
28954
|
return {
|
|
26513
|
-
type
|
|
28955
|
+
type,
|
|
28956
|
+
bindingName: binding.name,
|
|
26514
28957
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
26515
28958
|
loadedAt: new Date()
|
|
26516
28959
|
};
|
|
26517
28960
|
}
|
|
26518
28961
|
}
|
|
26519
28962
|
/**
|
|
26520
|
-
* Loads data from a runtime entity binding
|
|
28963
|
+
* Loads data from a runtime entity binding. A fixed `rtId` (variables
|
|
28964
|
+
* resolved) wins; without one the current entity of the bound selector is
|
|
28965
|
+
* loaded (AB#8023): the first rtId of its resolved scope. A pending scope
|
|
28966
|
+
* gives a `pending` result, an empty scope or a selector without pick no
|
|
28967
|
+
* entity (no value, never the previous one).
|
|
26521
28968
|
*/
|
|
26522
|
-
async loadBoundRuntimeEntityData(
|
|
26523
|
-
|
|
26524
|
-
if (
|
|
26525
|
-
|
|
28969
|
+
async loadBoundRuntimeEntityData(binding) {
|
|
28970
|
+
let rtId;
|
|
28971
|
+
if (binding.rtId) {
|
|
28972
|
+
// Resolve variables in rtId (e.g., ${selectedEntity})
|
|
28973
|
+
rtId = this.resolveVariables(binding.rtId);
|
|
26526
28974
|
}
|
|
26527
|
-
|
|
26528
|
-
|
|
28975
|
+
else {
|
|
28976
|
+
const scope = this.stateService.resolveEntitySelectorScope(binding.entitySelectorId);
|
|
28977
|
+
if (scope.kind === 'pending') {
|
|
28978
|
+
return { type: 'entity', pending: true, loadedAt: new Date() };
|
|
28979
|
+
}
|
|
28980
|
+
if (scope.kind !== 'scoped') {
|
|
28981
|
+
return { type: 'entity', loadedAt: new Date() };
|
|
28982
|
+
}
|
|
28983
|
+
rtId = scope.rtIds[0];
|
|
28984
|
+
}
|
|
28985
|
+
const ckTypeId = binding.ckTypeId ?? this.selectorCkTypeId(binding.entitySelectorId);
|
|
28986
|
+
if (!ckTypeId) {
|
|
28987
|
+
throw new Error('Runtime entity binding requires a CK Type ID');
|
|
26529
28988
|
}
|
|
26530
|
-
// Resolve variables in rtId (e.g., ${selectedEntity})
|
|
26531
|
-
const resolvedRtId = this.resolveVariables(bindingRtId);
|
|
26532
28989
|
// Fetch the entity
|
|
26533
|
-
const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(
|
|
28990
|
+
const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(rtId, ckTypeId));
|
|
26534
28991
|
if (!entity) {
|
|
26535
|
-
throw new Error(`Entity not found: ${
|
|
28992
|
+
throw new Error(`Entity not found: ${rtId}`);
|
|
26536
28993
|
}
|
|
26537
28994
|
return {
|
|
26538
28995
|
type: 'entity',
|
|
@@ -26540,39 +28997,50 @@ class ProcessDataService {
|
|
|
26540
28997
|
loadedAt: new Date()
|
|
26541
28998
|
};
|
|
26542
28999
|
}
|
|
29000
|
+
/**
|
|
29001
|
+
* CK type of the entity a selector's scope consists of — known only for a
|
|
29002
|
+
* selector without `childScope` (its scope is the picked entity itself).
|
|
29003
|
+
*/
|
|
29004
|
+
selectorCkTypeId(selectorId) {
|
|
29005
|
+
if (!selectorId) {
|
|
29006
|
+
return undefined;
|
|
29007
|
+
}
|
|
29008
|
+
const selector = this.stateService.getConfig().entitySelectors?.find(s => s.id === selectorId);
|
|
29009
|
+
return selector && !selector.childScope ? selector.ckTypeId : undefined;
|
|
29010
|
+
}
|
|
26543
29011
|
/**
|
|
26544
29012
|
* Loads data from a persistent query binding
|
|
26545
29013
|
*/
|
|
26546
|
-
async loadBoundQueryData(
|
|
26547
|
-
const {
|
|
26548
|
-
if (!
|
|
29014
|
+
async loadBoundQueryData(binding) {
|
|
29015
|
+
const { queryRtId, filters, queryFamily } = binding;
|
|
29016
|
+
if (!queryRtId) {
|
|
26549
29017
|
throw new Error('Persistent query binding requires a Query rtId');
|
|
26550
29018
|
}
|
|
26551
29019
|
// Entity-selector scope (AB#7721): NO query is sent while the scope is pending
|
|
26552
29020
|
// or empty — never the persisted (unscoped) scope. Pending → a `pending` result
|
|
26553
29021
|
// (the widget stays loading; a refresh follows once the scope resolves);
|
|
26554
29022
|
// empty → no rows.
|
|
26555
|
-
const scope = this.stateService.resolveStreamDataScope(
|
|
29023
|
+
const scope = this.stateService.resolveStreamDataScope(binding.entitySelectorId, queryFamily, binding.ignoreTimeFilter);
|
|
26556
29024
|
if (scope.kind === 'pending') {
|
|
26557
29025
|
return { type: 'query', pending: true, loadedAt: new Date() };
|
|
26558
29026
|
}
|
|
26559
29027
|
if (scope.kind === 'empty') {
|
|
26560
29028
|
return {
|
|
26561
29029
|
type: 'query',
|
|
26562
|
-
queryResult: this.mapExecutionResult(emptyQueryExecutionResult(
|
|
29030
|
+
queryResult: this.mapExecutionResult(emptyQueryExecutionResult(queryFamily)),
|
|
26563
29031
|
loadedAt: new Date()
|
|
26564
29032
|
};
|
|
26565
29033
|
}
|
|
26566
29034
|
// Convert widget filters to GraphQL format with variable resolution
|
|
26567
|
-
const fieldFilters = this.convertFiltersToDto(
|
|
29035
|
+
const fieldFilters = this.convertFiltersToDto(filters);
|
|
26568
29036
|
// Execute via the unified executor — it routes runtime vs stream-data from
|
|
26569
29037
|
// the persisted family (or a one-time lookup when absent). Stream-data
|
|
26570
29038
|
// queries additionally consume the time-filter / asset-scope overrides built
|
|
26571
29039
|
// below; runtime queries ignore `streamDataArgs`.
|
|
26572
|
-
const result = await firstValueFrom(this.queryExecutor.execute(
|
|
29040
|
+
const result = await firstValueFrom(this.queryExecutor.execute(queryFamily, queryRtId, {
|
|
26573
29041
|
first: 1000, // Reasonable limit for process diagram data
|
|
26574
29042
|
fieldFilter: fieldFilters,
|
|
26575
|
-
streamDataArgs: this.buildBindingStreamDataArgs(
|
|
29043
|
+
streamDataArgs: this.buildBindingStreamDataArgs(binding),
|
|
26576
29044
|
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
26577
29045
|
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
26578
29046
|
}));
|
|
@@ -26588,9 +29056,9 @@ class ProcessDataService {
|
|
|
26588
29056
|
* Returns `undefined` when neither override applies (e.g. runtime query, no
|
|
26589
29057
|
* time filter, opted out). Runtime queries ignore the result.
|
|
26590
29058
|
*/
|
|
26591
|
-
buildBindingStreamDataArgs(
|
|
26592
|
-
const timeArgs = this.stateService.resolveStreamDataTimeArgs(
|
|
26593
|
-
const rtIds = this.stateService.resolveStreamDataRtIds(
|
|
29059
|
+
buildBindingStreamDataArgs(binding) {
|
|
29060
|
+
const timeArgs = this.stateService.resolveStreamDataTimeArgs(binding.ignoreTimeFilter);
|
|
29061
|
+
const rtIds = this.stateService.resolveStreamDataRtIds(binding.entitySelectorId);
|
|
26594
29062
|
if (!timeArgs && !rtIds) {
|
|
26595
29063
|
return undefined;
|
|
26596
29064
|
}
|
|
@@ -26668,9 +29136,12 @@ class ProcessWidgetComponent {
|
|
|
26668
29136
|
config;
|
|
26669
29137
|
containerRef;
|
|
26670
29138
|
dataService = inject(ProcessDataService);
|
|
29139
|
+
stateService = inject(MeshBoardStateService);
|
|
26671
29140
|
symbolLibraryService = inject(SymbolLibraryService);
|
|
26672
29141
|
sanitizer = inject(DomSanitizer);
|
|
26673
29142
|
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
29143
|
+
/** Host language for `text.value` bindings (AB#8022); no language = de-AT. */
|
|
29144
|
+
hostLanguage = injectMeshBoardLanguage();
|
|
26674
29145
|
// Math reference for template
|
|
26675
29146
|
Math = Math;
|
|
26676
29147
|
// State signals
|
|
@@ -26686,6 +29157,16 @@ class ProcessWidgetComponent {
|
|
|
26686
29157
|
...(ngDevMode ? [{ debugName: "_symbolDefinitions" }] : /* istanbul ignore next */ []));
|
|
26687
29158
|
_boundData = signal(null, /* @ts-ignore */
|
|
26688
29159
|
...(ngDevMode ? [{ debugName: "_boundData" }] : /* istanbul ignore next */ []));
|
|
29160
|
+
/** Loaded results per binding name (AB#8023); pending bindings are absent. */
|
|
29161
|
+
_boundDataByBinding = signal(new Map(), /* @ts-ignore */
|
|
29162
|
+
...(ngDevMode ? [{ debugName: "_boundDataByBinding" }] : /* istanbul ignore next */ []));
|
|
29163
|
+
/** Console messages already written by this widget instance (each is reported once). */
|
|
29164
|
+
reportedWarnings = new Set();
|
|
29165
|
+
/**
|
|
29166
|
+
* Generation of the latest `loadDiagram()` call: a slower, older load (e.g. of
|
|
29167
|
+
* the previous leader pick) never overwrites the bound data of a newer one.
|
|
29168
|
+
*/
|
|
29169
|
+
loadGeneration = 0;
|
|
26689
29170
|
isLoading = this._isLoading.asReadonly();
|
|
26690
29171
|
error = this._error.asReadonly();
|
|
26691
29172
|
diagramConfig = this._diagramConfig.asReadonly();
|
|
@@ -26696,6 +29177,30 @@ class ProcessWidgetComponent {
|
|
|
26696
29177
|
* Contains entity or query data that can be used for dynamic values in the diagram.
|
|
26697
29178
|
*/
|
|
26698
29179
|
boundData = this._boundData.asReadonly();
|
|
29180
|
+
/** Loaded results per binding name (AB#8023). Pending bindings have no entry. */
|
|
29181
|
+
boundDataByBinding = this._boundDataByBinding.asReadonly();
|
|
29182
|
+
/**
|
|
29183
|
+
* Bindings whose load failed, for the error badge (AB#8023). Only for widgets
|
|
29184
|
+
* with named `bindings`: a legacy single-binding widget keeps its previous
|
|
29185
|
+
* behaviour (diagram without values, no badge).
|
|
29186
|
+
*/
|
|
29187
|
+
bindingErrors = computed(() => {
|
|
29188
|
+
const errors = [];
|
|
29189
|
+
const results = this._boundDataByBinding();
|
|
29190
|
+
if (!hasNamedProcessBindings(this.config)) {
|
|
29191
|
+
return errors;
|
|
29192
|
+
}
|
|
29193
|
+
for (const [name, result] of results) {
|
|
29194
|
+
if (result.error) {
|
|
29195
|
+
errors.push({ name, message: result.error });
|
|
29196
|
+
}
|
|
29197
|
+
}
|
|
29198
|
+
return errors;
|
|
29199
|
+
}, /* @ts-ignore */
|
|
29200
|
+
...(ngDevMode ? [{ debugName: "bindingErrors" }] : /* istanbul ignore next */ []));
|
|
29201
|
+
/** Text of the error badge: "Binding pv: <message>; Binding netz: …". */
|
|
29202
|
+
bindingErrorText = computed(() => this.bindingErrors().map(e => `Binding ${e.name}: ${e.message}`).join('; '), /* @ts-ignore */
|
|
29203
|
+
...(ngDevMode ? [{ debugName: "bindingErrorText" }] : /* istanbul ignore next */ []));
|
|
26699
29204
|
// Computed SVG properties
|
|
26700
29205
|
viewBox = computed(() => {
|
|
26701
29206
|
const diagram = this._diagramConfig();
|
|
@@ -26723,7 +29228,7 @@ class ProcessWidgetComponent {
|
|
|
26723
29228
|
animationEnabledStates = computed(() => {
|
|
26724
29229
|
const states = {};
|
|
26725
29230
|
const diagram = this._diagramConfig();
|
|
26726
|
-
const boundData = this.
|
|
29231
|
+
const boundData = this._boundDataByBinding();
|
|
26727
29232
|
if (!diagram?.propertyBindings || diagram.propertyBindings.length === 0) {
|
|
26728
29233
|
return states;
|
|
26729
29234
|
}
|
|
@@ -26733,7 +29238,7 @@ class ProcessWidgetComponent {
|
|
|
26733
29238
|
return states;
|
|
26734
29239
|
}
|
|
26735
29240
|
// Build property values from bound data
|
|
26736
|
-
const propertyValues = boundData
|
|
29241
|
+
const propertyValues = boundData.size > 0
|
|
26737
29242
|
? this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData)
|
|
26738
29243
|
: {};
|
|
26739
29244
|
// Evaluate each animation binding
|
|
@@ -26761,18 +29266,24 @@ class ProcessWidgetComponent {
|
|
|
26761
29266
|
*/
|
|
26762
29267
|
boundPrimitives = computed(() => {
|
|
26763
29268
|
const diagram = this._diagramConfig();
|
|
26764
|
-
const boundData = this.
|
|
29269
|
+
const boundData = this._boundDataByBinding();
|
|
26765
29270
|
if (!diagram?.primitives) {
|
|
26766
29271
|
return [];
|
|
26767
29272
|
}
|
|
26768
|
-
// If no bindings
|
|
26769
|
-
|
|
29273
|
+
// If no bindings, return original primitives. Without bound data only a
|
|
29274
|
+
// text.value binding changes anything: it shows "–" instead of the static
|
|
29275
|
+
// design-time text (AB#8022); every other effect waits for data as before.
|
|
29276
|
+
if (!diagram.propertyBindings || diagram.propertyBindings.length === 0) {
|
|
29277
|
+
return diagram.primitives;
|
|
29278
|
+
}
|
|
29279
|
+
if (boundData.size === 0 && !diagram.propertyBindings.some(b => b.effectType === 'text.value')) {
|
|
26770
29280
|
return diagram.primitives;
|
|
26771
29281
|
}
|
|
26772
29282
|
// Build property values from bound data using mappings
|
|
26773
|
-
const
|
|
29283
|
+
const noValue = new Set();
|
|
29284
|
+
const propertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, noValue);
|
|
26774
29285
|
// Apply bindings to primitives
|
|
26775
|
-
return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues);
|
|
29286
|
+
return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues, diagram.transformProperties, noValue);
|
|
26776
29287
|
}, /* @ts-ignore */
|
|
26777
29288
|
...(ngDevMode ? [{ debugName: "boundPrimitives" }] : /* istanbul ignore next */ []));
|
|
26778
29289
|
ngOnInit() {
|
|
@@ -26790,9 +29301,11 @@ class ProcessWidgetComponent {
|
|
|
26790
29301
|
* Loads the process diagram configuration and data
|
|
26791
29302
|
*/
|
|
26792
29303
|
async loadDiagram() {
|
|
29304
|
+
const generation = ++this.loadGeneration;
|
|
26793
29305
|
this._isLoading.set(true);
|
|
26794
29306
|
this._error.set(null);
|
|
26795
29307
|
this._boundData.set(null);
|
|
29308
|
+
this._boundDataByBinding.set(new Map());
|
|
26796
29309
|
// A bound entity-selector scope that is still resolving keeps the widget loading
|
|
26797
29310
|
// until the refresh that follows the resolution (AB#7721).
|
|
26798
29311
|
let scopePending = false;
|
|
@@ -26802,7 +29315,7 @@ class ProcessWidgetComponent {
|
|
|
26802
29315
|
this._diagramConfig.set(this.config.inlineConfig);
|
|
26803
29316
|
await this.loadSymbolDefinitions(this.config.inlineConfig);
|
|
26804
29317
|
await this.loadRuntimeData();
|
|
26805
|
-
scopePending = await this.loadBoundData();
|
|
29318
|
+
scopePending = await this.loadBoundData(generation);
|
|
26806
29319
|
return;
|
|
26807
29320
|
}
|
|
26808
29321
|
// Load from backend by rtId
|
|
@@ -26811,7 +29324,7 @@ class ProcessWidgetComponent {
|
|
|
26811
29324
|
this._diagramConfig.set(diagram);
|
|
26812
29325
|
await this.loadSymbolDefinitions(diagram);
|
|
26813
29326
|
await this.loadRuntimeData();
|
|
26814
|
-
scopePending = await this.loadBoundData();
|
|
29327
|
+
scopePending = await this.loadBoundData(generation);
|
|
26815
29328
|
return;
|
|
26816
29329
|
}
|
|
26817
29330
|
// No configuration
|
|
@@ -26822,7 +29335,7 @@ class ProcessWidgetComponent {
|
|
|
26822
29335
|
this._error.set('Failed to load process diagram');
|
|
26823
29336
|
}
|
|
26824
29337
|
finally {
|
|
26825
|
-
if (!scopePending) {
|
|
29338
|
+
if (!scopePending && generation === this.loadGeneration) {
|
|
26826
29339
|
this._isLoading.set(false);
|
|
26827
29340
|
}
|
|
26828
29341
|
}
|
|
@@ -26831,17 +29344,34 @@ class ProcessWidgetComponent {
|
|
|
26831
29344
|
* Loads bound data based on widget data binding configuration.
|
|
26832
29345
|
* Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
|
|
26833
29346
|
*/
|
|
26834
|
-
async loadBoundData() {
|
|
26835
|
-
|
|
26836
|
-
if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
|
|
26837
|
-
return false;
|
|
26838
|
-
}
|
|
29347
|
+
async loadBoundData(generation) {
|
|
29348
|
+
this.reportBindingIssues();
|
|
26839
29349
|
try {
|
|
26840
|
-
|
|
26841
|
-
|
|
29350
|
+
// Every binding loads in parallel (AB#8023): the named `bindings`, or the
|
|
29351
|
+
// implicit legacy binding `default`. No binding configured → nothing to load.
|
|
29352
|
+
const results = await this.dataService.loadBindings(this.config);
|
|
29353
|
+
if (generation !== this.loadGeneration) {
|
|
29354
|
+
// A newer load started meanwhile (e.g. leader pick changed): drop this result.
|
|
29355
|
+
return false;
|
|
29356
|
+
}
|
|
29357
|
+
if (results.size === 0) {
|
|
29358
|
+
return false;
|
|
29359
|
+
}
|
|
29360
|
+
// A pending binding (selector scope still resolving) gets no value and does
|
|
29361
|
+
// not blank the others. Only when every binding is pending does the widget
|
|
29362
|
+
// stay loading until the refresh that follows the resolution (AB#7721) —
|
|
29363
|
+
// for a legacy widget that is exactly its single binding.
|
|
29364
|
+
const loaded = new Map();
|
|
29365
|
+
for (const [name, result] of results) {
|
|
29366
|
+
if (!result.pending) {
|
|
29367
|
+
loaded.set(name, result);
|
|
29368
|
+
}
|
|
29369
|
+
}
|
|
29370
|
+
if (loaded.size === 0) {
|
|
26842
29371
|
return true;
|
|
26843
29372
|
}
|
|
26844
|
-
this.
|
|
29373
|
+
this._boundDataByBinding.set(loaded);
|
|
29374
|
+
this._boundData.set(loaded.get(DEFAULT_PROCESS_BINDING_NAME) ?? null);
|
|
26845
29375
|
}
|
|
26846
29376
|
catch (err) {
|
|
26847
29377
|
console.error('Error loading bound data:', err);
|
|
@@ -26849,6 +29379,22 @@ class ProcessWidgetComponent {
|
|
|
26849
29379
|
}
|
|
26850
29380
|
return false;
|
|
26851
29381
|
}
|
|
29382
|
+
/** Writes the config issues of the bindings to the console, each once per widget instance. */
|
|
29383
|
+
reportBindingIssues() {
|
|
29384
|
+
const selectorIds = (this.stateService.getConfig().entitySelectors ?? []).map(s => s.id);
|
|
29385
|
+
const properties = this._diagramConfig()?.transformProperties;
|
|
29386
|
+
const propertyTypes = properties ? new Map(properties.map(p => [p.id, p.type])) : undefined;
|
|
29387
|
+
for (const issue of validateProcessWidgetBindings(this.config, selectorIds, propertyTypes)) {
|
|
29388
|
+
this.warnOnce(`[ProcessWidget] ${this.config.title || this.config.id}: ${issue.message}`);
|
|
29389
|
+
}
|
|
29390
|
+
}
|
|
29391
|
+
warnOnce(message) {
|
|
29392
|
+
if (this.reportedWarnings.has(message)) {
|
|
29393
|
+
return;
|
|
29394
|
+
}
|
|
29395
|
+
this.reportedWarnings.add(message);
|
|
29396
|
+
console.warn(message);
|
|
29397
|
+
}
|
|
26852
29398
|
/**
|
|
26853
29399
|
* Loads symbol definitions for all symbol instances in the diagram
|
|
26854
29400
|
*/
|
|
@@ -27384,11 +29930,12 @@ class ProcessWidgetComponent {
|
|
|
27384
29930
|
return [];
|
|
27385
29931
|
}
|
|
27386
29932
|
// Get property values for this symbol instance
|
|
27387
|
-
const
|
|
29933
|
+
const noValue = new Set();
|
|
29934
|
+
const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition, noValue);
|
|
27388
29935
|
// If there are bindings, apply them
|
|
27389
29936
|
let primitives;
|
|
27390
29937
|
if (definition.propertyBindings && definition.propertyBindings.length > 0) {
|
|
27391
|
-
primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues);
|
|
29938
|
+
primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues, definition.transformProperties, noValue);
|
|
27392
29939
|
}
|
|
27393
29940
|
else {
|
|
27394
29941
|
primitives = [...definition.primitives];
|
|
@@ -27396,7 +29943,7 @@ class ProcessWidgetComponent {
|
|
|
27396
29943
|
// Add primitives from nested symbol instances (recursively)
|
|
27397
29944
|
const nestedInstances = definition.symbolInstances ?? [];
|
|
27398
29945
|
for (const nestedInst of nestedInstances) {
|
|
27399
|
-
const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues);
|
|
29946
|
+
const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues, noValue);
|
|
27400
29947
|
primitives.push(...nestedPrimitives);
|
|
27401
29948
|
}
|
|
27402
29949
|
return primitives;
|
|
@@ -27404,18 +29951,19 @@ class ProcessWidgetComponent {
|
|
|
27404
29951
|
/**
|
|
27405
29952
|
* Gets primitives for a nested symbol instance, with position offset applied.
|
|
27406
29953
|
*/
|
|
27407
|
-
getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues) {
|
|
29954
|
+
getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues, parentNoValue = new Set()) {
|
|
27408
29955
|
const definitions = this._symbolDefinitions();
|
|
27409
29956
|
const nestedDef = definitions.get(nestedInst.symbolRtId);
|
|
27410
29957
|
if (!nestedDef?.primitives) {
|
|
27411
29958
|
return [];
|
|
27412
29959
|
}
|
|
27413
29960
|
// Calculate property values for the nested instance using "Pass to child property" bindings
|
|
27414
|
-
const
|
|
29961
|
+
const nestedNoValue = new Set();
|
|
29962
|
+
const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue, nestedNoValue);
|
|
27415
29963
|
// Apply bindings to nested primitives
|
|
27416
29964
|
let nestedPrimitives;
|
|
27417
29965
|
if (nestedDef.propertyBindings && nestedDef.propertyBindings.length > 0) {
|
|
27418
|
-
nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues);
|
|
29966
|
+
nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues, nestedDef.transformProperties, nestedNoValue);
|
|
27419
29967
|
}
|
|
27420
29968
|
else {
|
|
27421
29969
|
nestedPrimitives = [...nestedDef.primitives];
|
|
@@ -27433,7 +29981,7 @@ class ProcessWidgetComponent {
|
|
|
27433
29981
|
// Recursively handle nested instances within the nested symbol
|
|
27434
29982
|
const deepNestedInstances = nestedDef.symbolInstances ?? [];
|
|
27435
29983
|
for (const deepNestedInst of deepNestedInstances) {
|
|
27436
|
-
const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues);
|
|
29984
|
+
const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues, nestedNoValue);
|
|
27437
29985
|
// Apply the parent offset to deep nested primitives
|
|
27438
29986
|
const deepOffsetPrimitives = deepPrimitives.map(prim => ({
|
|
27439
29987
|
...prim,
|
|
@@ -27449,7 +29997,7 @@ class ProcessWidgetComponent {
|
|
|
27449
29997
|
/**
|
|
27450
29998
|
* Calculate property values for a nested symbol instance based on "Pass to child property" bindings.
|
|
27451
29999
|
*/
|
|
27452
|
-
calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues) {
|
|
30000
|
+
calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue = new Set(), nestedNoValue) {
|
|
27453
30001
|
const nestedPropertyValues = {};
|
|
27454
30002
|
// Get default values from nested symbol definition
|
|
27455
30003
|
const definitions = this._symbolDefinitions();
|
|
@@ -27468,6 +30016,10 @@ class ProcessWidgetComponent {
|
|
|
27468
30016
|
b.targetType === 'symbolInstance' &&
|
|
27469
30017
|
b.targetId === nestedInst.id) ?? [];
|
|
27470
30018
|
for (const binding of passThroughBindings) {
|
|
30019
|
+
// A parent property without a value leaves the child property without one (AB#8022)
|
|
30020
|
+
if (binding.targetPropertyId && parentNoValue.has(binding.propertyId)) {
|
|
30021
|
+
nestedNoValue?.add(binding.targetPropertyId);
|
|
30022
|
+
}
|
|
27471
30023
|
const sourceValue = parentPropertyValues[binding.propertyId];
|
|
27472
30024
|
if (sourceValue !== undefined && binding.targetPropertyId) {
|
|
27473
30025
|
// Evaluate expression if present
|
|
@@ -27552,7 +30104,7 @@ class ProcessWidgetComponent {
|
|
|
27552
30104
|
* Gets property values for a symbol instance.
|
|
27553
30105
|
* Combines default values with any values passed from parent bindings.
|
|
27554
30106
|
*/
|
|
27555
|
-
getSymbolInstancePropertyValues(symbolInstance, definition) {
|
|
30107
|
+
getSymbolInstancePropertyValues(symbolInstance, definition, noValue) {
|
|
27556
30108
|
const values = {};
|
|
27557
30109
|
// Initialize with default values from transform properties
|
|
27558
30110
|
for (const prop of definition.transformProperties ?? []) {
|
|
@@ -27560,16 +30112,26 @@ class ProcessWidgetComponent {
|
|
|
27560
30112
|
}
|
|
27561
30113
|
// Override with values from parent diagram bindings (via 'property' effect type)
|
|
27562
30114
|
const diagram = this._diagramConfig();
|
|
27563
|
-
const boundData = this.
|
|
27564
|
-
if (diagram?.propertyBindings
|
|
30115
|
+
const boundData = this._boundDataByBinding();
|
|
30116
|
+
if (diagram?.propertyBindings) {
|
|
27565
30117
|
// Get parent property values
|
|
27566
|
-
const
|
|
30118
|
+
const parentNoValue = new Set();
|
|
30119
|
+
const parentPropertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, parentNoValue);
|
|
27567
30120
|
// Find bindings that pass values to this symbol instance
|
|
27568
30121
|
const propertyBindings = diagram.propertyBindings.filter(b => b.targetType === 'symbolInstance' &&
|
|
27569
30122
|
b.targetId === symbolInstance.id &&
|
|
27570
30123
|
b.effectType === 'property' &&
|
|
27571
30124
|
b.targetPropertyId);
|
|
27572
30125
|
for (const binding of propertyBindings) {
|
|
30126
|
+
// A parent property without a value leaves the symbol property without one,
|
|
30127
|
+
// so a text.value binding inside the symbol shows "–" (AB#8022)
|
|
30128
|
+
if (parentNoValue.has(binding.propertyId)) {
|
|
30129
|
+
noValue?.add(binding.targetPropertyId);
|
|
30130
|
+
}
|
|
30131
|
+
// Without bound data the symbol keeps its defaults, as before
|
|
30132
|
+
if (boundData.size === 0) {
|
|
30133
|
+
continue;
|
|
30134
|
+
}
|
|
27573
30135
|
const parentValue = parentPropertyValues[binding.propertyId];
|
|
27574
30136
|
if (parentValue === undefined) {
|
|
27575
30137
|
continue;
|
|
@@ -27990,7 +30552,7 @@ class ProcessWidgetComponent {
|
|
|
27990
30552
|
* Maps diagram exposed properties to values from entity attributes or query columns.
|
|
27991
30553
|
* Applies optional expressions for value transformation/normalization.
|
|
27992
30554
|
*/
|
|
27993
|
-
buildPropertyValuesFromBoundData(transformProperties, mappings,
|
|
30555
|
+
buildPropertyValuesFromBoundData(transformProperties, mappings, boundDataByBinding, noValue) {
|
|
27994
30556
|
const values = {};
|
|
27995
30557
|
// Initialize with default values from transform properties
|
|
27996
30558
|
for (const prop of transformProperties) {
|
|
@@ -28001,14 +30563,42 @@ class ProcessWidgetComponent {
|
|
|
28001
30563
|
const prop = transformProperties.find(p => p.id === mapping.propertyId);
|
|
28002
30564
|
if (!prop)
|
|
28003
30565
|
continue;
|
|
30566
|
+
// Each mapping reads from its own binding (AB#8023). A pending binding has
|
|
30567
|
+
// no entry and leaves the default; an unknown name is reported once.
|
|
30568
|
+
const boundData = boundDataByBinding.get(mappingBindingName(mapping));
|
|
30569
|
+
if (!boundData) {
|
|
30570
|
+
// No data yet: a text.value binding shows "–", other effects keep the default
|
|
30571
|
+
noValue?.add(mapping.propertyId);
|
|
30572
|
+
continue;
|
|
30573
|
+
}
|
|
28004
30574
|
let rawValue = undefined;
|
|
28005
30575
|
if (mapping.sourceType === 'attribute' && boundData.type === 'entity' && boundData.entity) {
|
|
28006
30576
|
// Extract value from entity attribute
|
|
28007
30577
|
rawValue = this.extractAttributeValue(boundData.entity, mapping.sourcePath);
|
|
28008
30578
|
}
|
|
28009
30579
|
else if (mapping.sourceType === 'column' && boundData.type === 'query' && boundData.queryResult) {
|
|
28010
|
-
//
|
|
28011
|
-
|
|
30580
|
+
// First row, or the aggregate over all rows of the scope (AB#8024).
|
|
30581
|
+
// An aggregate on a non-number property falls back to the first row;
|
|
30582
|
+
// the validator has reported it once.
|
|
30583
|
+
const aggregate = prop.type === 'number' ? mapping.aggregate : undefined;
|
|
30584
|
+
rawValue = aggregateQueryColumn(boundData.queryResult, mapping.sourcePath, aggregate);
|
|
30585
|
+
if (isReducingAggregate(aggregate) && isPartialAggregate(boundData.queryResult)) {
|
|
30586
|
+
this.warnOnce(`[ProcessWidget] ${this.config.title || this.config.id}: aggregate '${aggregate}' of binding ` +
|
|
30587
|
+
`'${mappingBindingName(mapping)}' covers ${boundData.queryResult.rows.length} of ` +
|
|
30588
|
+
`${boundData.queryResult.totalCount} rows`);
|
|
30589
|
+
}
|
|
30590
|
+
if (isReducingAggregate(aggregate) && rawValue === undefined) {
|
|
30591
|
+
// No row / no numeric cell: the property has no value (not 0, not
|
|
30592
|
+
// its default), so its effects are skipped and a text shows "–".
|
|
30593
|
+
values[mapping.propertyId] = undefined;
|
|
30594
|
+
noValue?.add(mapping.propertyId);
|
|
30595
|
+
continue;
|
|
30596
|
+
}
|
|
30597
|
+
}
|
|
30598
|
+
if (rawValue === undefined || rawValue === null) {
|
|
30599
|
+
// Missing attribute or empty cell (AB#8022). The values map keeps its
|
|
30600
|
+
// legacy content (default, or null coerced) for every other effect.
|
|
30601
|
+
noValue?.add(mapping.propertyId);
|
|
28012
30602
|
}
|
|
28013
30603
|
if (rawValue !== undefined) {
|
|
28014
30604
|
let finalValue = rawValue;
|
|
@@ -28032,6 +30622,11 @@ class ProcessWidgetComponent {
|
|
|
28032
30622
|
console.warn(`[ProcessWidget] Expression error for ${mapping.propertyId}:`, err);
|
|
28033
30623
|
}
|
|
28034
30624
|
}
|
|
30625
|
+
// A number property whose value is not numeric has no value for a text
|
|
30626
|
+
// (coerceToType turns it into 0, which a text must never show)
|
|
30627
|
+
if (prop.type === 'number' && !isNumericValue(finalValue)) {
|
|
30628
|
+
noValue?.add(mapping.propertyId);
|
|
30629
|
+
}
|
|
28035
30630
|
// Convert to expected type
|
|
28036
30631
|
values[mapping.propertyId] = this.coerceToType(finalValue, prop.type);
|
|
28037
30632
|
}
|
|
@@ -28082,17 +30677,6 @@ class ProcessWidgetComponent {
|
|
|
28082
30677
|
}
|
|
28083
30678
|
return value;
|
|
28084
30679
|
}
|
|
28085
|
-
/**
|
|
28086
|
-
* Extracts a column value from query results (first row).
|
|
28087
|
-
*/
|
|
28088
|
-
extractQueryColumnValue(queryResult, columnName) {
|
|
28089
|
-
if (!queryResult.rows || queryResult.rows.length === 0)
|
|
28090
|
-
return undefined;
|
|
28091
|
-
const firstRow = queryResult.rows[0];
|
|
28092
|
-
// Try both the column name and sanitized version (dots -> underscores)
|
|
28093
|
-
const value = firstRow.cells.get(columnName) ?? firstRow.cells.get(columnName.replace(/\./g, '_'));
|
|
28094
|
-
return value;
|
|
28095
|
-
}
|
|
28096
30680
|
/**
|
|
28097
30681
|
* Coerces a value to the expected type.
|
|
28098
30682
|
*/
|
|
@@ -28124,7 +30708,7 @@ class ProcessWidgetComponent {
|
|
|
28124
30708
|
* Applies property bindings to primitives.
|
|
28125
30709
|
* Creates clones of primitives with binding effects applied.
|
|
28126
30710
|
*/
|
|
28127
|
-
applyBindingsToPrimitives(primitives, bindings, propertyValues) {
|
|
30711
|
+
applyBindingsToPrimitives(primitives, bindings, propertyValues, properties, noValue) {
|
|
28128
30712
|
return primitives.map(primitive => {
|
|
28129
30713
|
// Find bindings for this primitive
|
|
28130
30714
|
const primitiveBindings = bindings.filter(b => b.targetType === 'primitive' && b.targetId === primitive.id);
|
|
@@ -28135,6 +30719,17 @@ class ProcessWidgetComponent {
|
|
|
28135
30719
|
let modified = this.clonePrimitive(primitive);
|
|
28136
30720
|
for (const binding of primitiveBindings) {
|
|
28137
30721
|
const propertyValue = propertyValues[binding.propertyId];
|
|
30722
|
+
if (binding.effectType === 'text.value') {
|
|
30723
|
+
// Formatted live value; "–" without a value, never 0 or the static text (AB#8022)
|
|
30724
|
+
const input = noValue?.has(binding.propertyId) ? undefined : propertyValue;
|
|
30725
|
+
const text = evaluateTextValue(input, binding, {
|
|
30726
|
+
evaluator: this.expressionEvaluator,
|
|
30727
|
+
locale: this.hostLanguage() ?? WIDGET_NUMBER_LOCALE,
|
|
30728
|
+
property: properties?.find(p => p.id === binding.propertyId)
|
|
30729
|
+
});
|
|
30730
|
+
modified = applyTextValueToPrimitive(modified, text);
|
|
30731
|
+
continue;
|
|
30732
|
+
}
|
|
28138
30733
|
if (propertyValue === undefined)
|
|
28139
30734
|
continue;
|
|
28140
30735
|
// Build expression context
|
|
@@ -29241,8 +31836,14 @@ class ProcessWidgetComponent {
|
|
|
29241
31836
|
<span>No process diagram configured</span>
|
|
29242
31837
|
</div>
|
|
29243
31838
|
}
|
|
31839
|
+
@if (!isLoading() && !error() && bindingErrors().length > 0) {
|
|
31840
|
+
<div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
|
|
31841
|
+
<span class="binding-error-icon" aria-hidden="true">⚠</span>
|
|
31842
|
+
<span class="binding-error-text">{{ bindingErrorText() }}</span>
|
|
31843
|
+
</div>
|
|
31844
|
+
}
|
|
29244
31845
|
</div>
|
|
29245
|
-
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
31846
|
+
`, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
|
|
29246
31847
|
}
|
|
29247
31848
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ProcessWidgetComponent, decorators: [{
|
|
29248
31849
|
type: Component,
|
|
@@ -30242,14 +32843,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
30242
32843
|
<span>No process diagram configured</span>
|
|
30243
32844
|
</div>
|
|
30244
32845
|
}
|
|
32846
|
+
@if (!isLoading() && !error() && bindingErrors().length > 0) {
|
|
32847
|
+
<div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
|
|
32848
|
+
<span class="binding-error-icon" aria-hidden="true">⚠</span>
|
|
32849
|
+
<span class="binding-error-text">{{ bindingErrorText() }}</span>
|
|
32850
|
+
</div>
|
|
32851
|
+
}
|
|
30245
32852
|
</div>
|
|
30246
|
-
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
|
|
32853
|
+
`, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
|
|
30247
32854
|
}], propDecorators: { config: [{
|
|
30248
32855
|
type: Input
|
|
30249
32856
|
}], containerRef: [{
|
|
30250
32857
|
type: ViewChild,
|
|
30251
32858
|
args: ['containerRef']
|
|
30252
32859
|
}] } });
|
|
32860
|
+
/** Whether a mapped value is a finite number or a numeric string (AB#8022). */
|
|
32861
|
+
function isNumericValue(value) {
|
|
32862
|
+
if (typeof value === 'number') {
|
|
32863
|
+
return Number.isFinite(value);
|
|
32864
|
+
}
|
|
32865
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
32866
|
+
return Number.isFinite(Number(value.trim()));
|
|
32867
|
+
}
|
|
32868
|
+
return false;
|
|
32869
|
+
}
|
|
30253
32870
|
|
|
30254
32871
|
/**
|
|
30255
32872
|
* Configuration dialog for Process Widget.
|
|
@@ -31453,6 +34070,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
31453
34070
|
* - Import and call registerProcessWidget() only in routes/components that need it
|
|
31454
34071
|
* - Or use provideProcessWidget() in app config for eager loading
|
|
31455
34072
|
*/
|
|
34073
|
+
/**
|
|
34074
|
+
* The legacy single-binding fields of one binding named `default` (AB#8023):
|
|
34075
|
+
* a widget with exactly that one binding is saved in the legacy shape, so no
|
|
34076
|
+
* existing board is migrated silently.
|
|
34077
|
+
*/
|
|
34078
|
+
function legacyFieldsOf(binding) {
|
|
34079
|
+
return {
|
|
34080
|
+
dataBindingMode: binding.mode,
|
|
34081
|
+
bindingCkTypeId: binding.ckTypeId,
|
|
34082
|
+
bindingRtId: binding.rtId,
|
|
34083
|
+
bindingQueryRtId: binding.queryRtId,
|
|
34084
|
+
bindingQueryName: binding.queryName,
|
|
34085
|
+
bindingQueryFamily: binding.queryFamily,
|
|
34086
|
+
bindingIgnoreTimeFilter: binding.ignoreTimeFilter,
|
|
34087
|
+
bindingEntitySelectorId: binding.entitySelectorId,
|
|
34088
|
+
bindingFilters: binding.filters
|
|
34089
|
+
};
|
|
34090
|
+
}
|
|
34091
|
+
/**
|
|
34092
|
+
* Normalizes the binding fields before persisting: a single binding named
|
|
34093
|
+
* `default` collapses into the legacy fields; anything else keeps `bindings`.
|
|
34094
|
+
*/
|
|
34095
|
+
function normalizeBindings(widget) {
|
|
34096
|
+
if (!hasNamedProcessBindings(widget)) {
|
|
34097
|
+
return widget;
|
|
34098
|
+
}
|
|
34099
|
+
const bindings = widget.bindings;
|
|
34100
|
+
if (bindings.length === 1 && bindings[0].name === DEFAULT_PROCESS_BINDING_NAME) {
|
|
34101
|
+
const { bindings: _dropped, ...rest } = widget;
|
|
34102
|
+
return { ...rest, ...legacyFieldsOf(bindings[0]) };
|
|
34103
|
+
}
|
|
34104
|
+
return widget;
|
|
34105
|
+
}
|
|
31456
34106
|
/**
|
|
31457
34107
|
* Registers the Process Widget with the widget registry.
|
|
31458
34108
|
*
|
|
@@ -31508,7 +34158,19 @@ function registerProcessWidget(registry) {
|
|
|
31508
34158
|
initialBindingFilters: widget.bindingFilters,
|
|
31509
34159
|
initialPropertyMappings: widget.propertyMappings
|
|
31510
34160
|
}),
|
|
31511
|
-
|
|
34161
|
+
// A widget with named bindings (AB#8023) keeps its bindings and mappings: the
|
|
34162
|
+
// legacy dialog edits only the single-binding fields and would drop the
|
|
34163
|
+
// mappings when its own mode is 'none' (the editor for bindings is AB#8025).
|
|
34164
|
+
applyConfigResult: (widget, result) => hasNamedProcessBindings(widget) ? {
|
|
34165
|
+
...widget,
|
|
34166
|
+
processDiagramRtId: result.processDiagramRtId,
|
|
34167
|
+
inlineConfig: result.inlineConfig,
|
|
34168
|
+
fitToBounds: result.fitToBounds,
|
|
34169
|
+
allowZoom: result.allowZoom,
|
|
34170
|
+
allowPan: result.allowPan,
|
|
34171
|
+
showToolbar: result.showToolbar,
|
|
34172
|
+
initialZoom: result.initialZoom
|
|
34173
|
+
} : ({
|
|
31512
34174
|
...widget,
|
|
31513
34175
|
// Diagram selection
|
|
31514
34176
|
processDiagramRtId: result.processDiagramRtId,
|
|
@@ -31548,35 +34210,42 @@ function registerProcessWidget(registry) {
|
|
|
31548
34210
|
initialZoom: 1
|
|
31549
34211
|
}),
|
|
31550
34212
|
// SOLID: Serialization for persistence
|
|
31551
|
-
toPersistedConfig: (
|
|
31552
|
-
|
|
31553
|
-
|
|
31554
|
-
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
|
|
31564
|
-
|
|
31565
|
-
|
|
31566
|
-
|
|
31567
|
-
|
|
31568
|
-
|
|
31569
|
-
|
|
31570
|
-
|
|
31571
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
34213
|
+
toPersistedConfig: (input) => {
|
|
34214
|
+
const widget = normalizeBindings(input);
|
|
34215
|
+
return {
|
|
34216
|
+
dataSourceType: 'static',
|
|
34217
|
+
config: {
|
|
34218
|
+
// Diagram selection
|
|
34219
|
+
processDiagramRtId: widget.processDiagramRtId,
|
|
34220
|
+
inlineConfig: widget.inlineConfig,
|
|
34221
|
+
// Display options
|
|
34222
|
+
fitToBounds: widget.fitToBounds,
|
|
34223
|
+
allowZoom: widget.allowZoom,
|
|
34224
|
+
allowPan: widget.allowPan,
|
|
34225
|
+
showToolbar: widget.showToolbar,
|
|
34226
|
+
initialZoom: widget.initialZoom,
|
|
34227
|
+
// Data binding
|
|
34228
|
+
dataBindingMode: widget.dataBindingMode,
|
|
34229
|
+
bindingCkTypeId: widget.bindingCkTypeId,
|
|
34230
|
+
bindingRtId: widget.bindingRtId,
|
|
34231
|
+
bindingQueryRtId: widget.bindingQueryRtId,
|
|
34232
|
+
bindingQueryName: widget.bindingQueryName,
|
|
34233
|
+
// Stream-data binding — written only when set.
|
|
34234
|
+
...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
|
|
34235
|
+
...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
|
|
34236
|
+
...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
|
|
34237
|
+
bindingFilters: widget.bindingFilters,
|
|
34238
|
+
propertyMappings: widget.propertyMappings,
|
|
34239
|
+
// Named bindings (AB#8023) — written only when present, so legacy
|
|
34240
|
+
// widgets encode byte-identically.
|
|
34241
|
+
...(hasNamedProcessBindings(widget) && { bindings: toPersistedProcessBindings(widget.bindings) })
|
|
34242
|
+
}
|
|
34243
|
+
};
|
|
34244
|
+
},
|
|
31577
34245
|
// SOLID: Deserialization from persistence
|
|
31578
34246
|
fromPersistedConfig: (data, base) => {
|
|
31579
34247
|
const config = parseConfig(data);
|
|
34248
|
+
const bindings = parsePersistedProcessBindings(config['bindings']);
|
|
31580
34249
|
return {
|
|
31581
34250
|
...base,
|
|
31582
34251
|
rtId: data.rtId,
|
|
@@ -31604,7 +34273,9 @@ function registerProcessWidget(registry) {
|
|
|
31604
34273
|
bindingIgnoreTimeFilter: config['bindingIgnoreTimeFilter'],
|
|
31605
34274
|
bindingEntitySelectorId: config['bindingEntitySelectorId'],
|
|
31606
34275
|
bindingFilters: config['bindingFilters'],
|
|
31607
|
-
propertyMappings: config['propertyMappings']
|
|
34276
|
+
propertyMappings: config['propertyMappings'],
|
|
34277
|
+
// Named bindings (AB#8023); absent key stays absent.
|
|
34278
|
+
...(bindings && { bindings })
|
|
31608
34279
|
};
|
|
31609
34280
|
}
|
|
31610
34281
|
});
|
|
@@ -32745,7 +35416,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32745
35416
|
initialShowDataLabels: barWidget.showDataLabels,
|
|
32746
35417
|
initialColorThresholds: barWidget.colorThresholds,
|
|
32747
35418
|
initialDefaultBarColor: barWidget.defaultBarColor,
|
|
32748
|
-
initialFilters: barWidget.filters
|
|
35419
|
+
initialFilters: barWidget.filters,
|
|
35420
|
+
initialAdditionalSeries: barWidget.additionalSeries,
|
|
35421
|
+
initialPrimarySeries: barWidget.primarySeries
|
|
32749
35422
|
};
|
|
32750
35423
|
},
|
|
32751
35424
|
applyConfigResult: (widget, result) => {
|
|
@@ -32776,7 +35449,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32776
35449
|
showDataLabels: result.showDataLabels,
|
|
32777
35450
|
colorThresholds: result.colorThresholds,
|
|
32778
35451
|
defaultBarColor: result.defaultBarColor,
|
|
32779
|
-
filters
|
|
35452
|
+
filters,
|
|
35453
|
+
additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
|
|
35454
|
+
primarySeries: barPrimaryOrUndefined(result.primarySeries)
|
|
32780
35455
|
};
|
|
32781
35456
|
},
|
|
32782
35457
|
// SOLID: Factory function
|
|
@@ -32810,6 +35485,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32810
35485
|
valueScale: widget.valueScale,
|
|
32811
35486
|
colorThresholds: widget.colorThresholds,
|
|
32812
35487
|
defaultBarColor: widget.defaultBarColor,
|
|
35488
|
+
// AB#7953: written only when set, so a single-query bar chart encodes exactly as before.
|
|
35489
|
+
...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
|
|
35490
|
+
...(barPrimaryOrUndefined(widget.primarySeries) && { primarySeries: barPrimaryOrUndefined(widget.primarySeries) }),
|
|
32813
35491
|
queryName: widget.dataSource.queryName,
|
|
32814
35492
|
queryRtId: widget.dataSource.queryRtId,
|
|
32815
35493
|
...(widget.dataSource.queryFamily && {
|
|
@@ -32845,7 +35523,8 @@ function registerDefaultWidgets(registry) {
|
|
|
32845
35523
|
valueScale: config['valueScale'],
|
|
32846
35524
|
colorThresholds: config['colorThresholds'],
|
|
32847
35525
|
defaultBarColor: config['defaultBarColor'],
|
|
32848
|
-
filters: config['filters']
|
|
35526
|
+
filters: config['filters'],
|
|
35527
|
+
...barChartSeriesFromPersisted(config)
|
|
32849
35528
|
};
|
|
32850
35529
|
}
|
|
32851
35530
|
});
|
|
@@ -32881,7 +35560,10 @@ function registerDefaultWidgets(registry) {
|
|
|
32881
35560
|
initialShowMarkers: lineWidget.showMarkers,
|
|
32882
35561
|
initialShowDataBadge: lineWidget.showDataBadge,
|
|
32883
35562
|
initialReferenceLines: lineWidget.referenceLines,
|
|
32884
|
-
initialFilters: lineWidget.filters
|
|
35563
|
+
initialFilters: lineWidget.filters,
|
|
35564
|
+
initialAdditionalSeries: lineWidget.additionalSeries,
|
|
35565
|
+
initialStacked: lineWidget.stacked,
|
|
35566
|
+
initialPrimarySeries: lineWidget.primarySeries
|
|
32885
35567
|
};
|
|
32886
35568
|
},
|
|
32887
35569
|
applyConfigResult: (widget, result) => {
|
|
@@ -32917,7 +35599,10 @@ function registerDefaultWidgets(registry) {
|
|
|
32917
35599
|
showMarkers: result.showMarkers,
|
|
32918
35600
|
showDataBadge: result.showDataBadge,
|
|
32919
35601
|
referenceLines: result.referenceLines,
|
|
32920
|
-
filters
|
|
35602
|
+
filters,
|
|
35603
|
+
additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
|
|
35604
|
+
stacked: result.stacked || undefined,
|
|
35605
|
+
primarySeries: displayOptionsOrUndefined(result.primarySeries)
|
|
32921
35606
|
};
|
|
32922
35607
|
},
|
|
32923
35608
|
createDefaultConfig: (base) => ({
|
|
@@ -32968,7 +35653,12 @@ function registerDefaultWidgets(registry) {
|
|
|
32968
35653
|
...(widget.dataSource.aggregateSeriesByGroup && {
|
|
32969
35654
|
aggregateSeriesByGroup: true
|
|
32970
35655
|
}),
|
|
32971
|
-
filters: widget.filters
|
|
35656
|
+
filters: widget.filters,
|
|
35657
|
+
// AB#7951: written only when set, so a single-query chart encodes exactly as before.
|
|
35658
|
+
...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
|
|
35659
|
+
...(widget.stacked && { stacked: true }),
|
|
35660
|
+
// AB#7952: written only when a primary display option is not default.
|
|
35661
|
+
...(displayOptionsOrUndefined(widget.primarySeries) && { primarySeries: displayOptionsOrUndefined(widget.primarySeries) })
|
|
32972
35662
|
}
|
|
32973
35663
|
}),
|
|
32974
35664
|
fromPersistedConfig: (data, base) => {
|
|
@@ -32989,7 +35679,8 @@ function registerDefaultWidgets(registry) {
|
|
|
32989
35679
|
showMarkers: config['showMarkers'] ?? false,
|
|
32990
35680
|
showDataBadge: config['showDataBadge'] ?? true,
|
|
32991
35681
|
referenceLines: config['referenceLines'],
|
|
32992
|
-
filters: config['filters']
|
|
35682
|
+
filters: config['filters'],
|
|
35683
|
+
...lineChartSeriesFromPersisted(config)
|
|
32993
35684
|
};
|
|
32994
35685
|
}
|
|
32995
35686
|
});
|
|
@@ -34025,6 +36716,72 @@ function provideWidgetRegistrations(registrations) {
|
|
|
34025
36716
|
})
|
|
34026
36717
|
]);
|
|
34027
36718
|
}
|
|
36719
|
+
/**
|
|
36720
|
+
* Reads `additionalSeries` / `stacked` / `primarySeries` of a persisted line chart (AB#7951,
|
|
36721
|
+
* AB#7952). All keys are absent on a single-query chart, so the result is `{}` and the loaded config
|
|
36722
|
+
* has no such keys. Kept entries: query series with a key and a data source, formula series with a
|
|
36723
|
+
* key and a formula string (AB#7952). Entries of an unknown `kind` are dropped (a newer library may
|
|
36724
|
+
* store kinds this version does not know). The display options `visible` / `stacked` are kept only
|
|
36725
|
+
* as `false`.
|
|
36726
|
+
*/
|
|
36727
|
+
function lineChartSeriesFromPersisted(config) {
|
|
36728
|
+
const out = {};
|
|
36729
|
+
const raw = config['additionalSeries'];
|
|
36730
|
+
if (Array.isArray(raw)) {
|
|
36731
|
+
const series = raw.filter((s) => {
|
|
36732
|
+
if (!s || typeof s !== 'object')
|
|
36733
|
+
return false;
|
|
36734
|
+
const entry = s;
|
|
36735
|
+
if (typeof entry.key !== 'string')
|
|
36736
|
+
return false;
|
|
36737
|
+
if (entry.kind === 'query')
|
|
36738
|
+
return !!entry.dataSource && typeof entry.dataSource.queryRtId === 'string';
|
|
36739
|
+
if (entry.kind === 'formula')
|
|
36740
|
+
return typeof entry.formula === 'string';
|
|
36741
|
+
return false;
|
|
36742
|
+
});
|
|
36743
|
+
if (series.length > 0)
|
|
36744
|
+
out.additionalSeries = series;
|
|
36745
|
+
}
|
|
36746
|
+
if (config['stacked'] === true)
|
|
36747
|
+
out.stacked = true;
|
|
36748
|
+
const primary = config['primarySeries'];
|
|
36749
|
+
if (primary && typeof primary === 'object') {
|
|
36750
|
+
const options = displayOptionsOrUndefined(primary);
|
|
36751
|
+
if (options)
|
|
36752
|
+
out.primarySeries = options;
|
|
36753
|
+
}
|
|
36754
|
+
return out;
|
|
36755
|
+
}
|
|
36756
|
+
/**
|
|
36757
|
+
* Reads `additionalSeries` / `primarySeries` of a persisted bar chart (AB#7953) with the line chart
|
|
36758
|
+
* rules (unknown kinds dropped, display options only as `false`). The bar chart keeps only
|
|
36759
|
+
* `visible` of the primary options. Both keys are absent on a single-query bar chart.
|
|
36760
|
+
*/
|
|
36761
|
+
function barChartSeriesFromPersisted(config) {
|
|
36762
|
+
const line = lineChartSeriesFromPersisted({ additionalSeries: config['additionalSeries'], primarySeries: config['primarySeries'] });
|
|
36763
|
+
const out = {};
|
|
36764
|
+
if (line.additionalSeries)
|
|
36765
|
+
out.additionalSeries = line.additionalSeries;
|
|
36766
|
+
const primary = barPrimaryOrUndefined(line.primarySeries);
|
|
36767
|
+
if (primary)
|
|
36768
|
+
out.primarySeries = primary;
|
|
36769
|
+
return out;
|
|
36770
|
+
}
|
|
36771
|
+
/** Bar chart primary options: `{ visible: false }` or `undefined`. */
|
|
36772
|
+
function barPrimaryOrUndefined(options) {
|
|
36773
|
+
return options?.visible === false ? { visible: false } : undefined;
|
|
36774
|
+
}
|
|
36775
|
+
/** Display options reduced to their non-default (`false`) values; `undefined` when all are default. */
|
|
36776
|
+
function displayOptionsOrUndefined(options) {
|
|
36777
|
+
if (!options)
|
|
36778
|
+
return undefined;
|
|
36779
|
+
const out = {
|
|
36780
|
+
...(options.visible === false && { visible: false }),
|
|
36781
|
+
...(options.stacked === false && { stacked: false })
|
|
36782
|
+
};
|
|
36783
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
36784
|
+
}
|
|
34028
36785
|
|
|
34029
36786
|
/** True when the selector is configured to follow another selector. */
|
|
34030
36787
|
function isFollowerSelector(selector) {
|
|
@@ -43229,5 +45986,5 @@ function provideCockpitWidgets() {
|
|
|
43229
45986
|
* Generated bundle index. Do not edit.
|
|
43230
45987
|
*/
|
|
43231
45988
|
|
|
43232
|
-
export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
|
|
45989
|
+
export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartSeriesEditorComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, alignOnTimeGrid, blueprintUpdatesKpi, bucketStepMs, buildHourlyHistogram, categoryStatus, chartSeriesColors, circularReferenceMessage, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, evaluateSeriesFormulas, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, orderSeriesFormulas, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, seriesFormulaOperands, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, validateSeriesFormulas, versionKpi, withMeshBoardNavigation };
|
|
43233
45990
|
//# sourceMappingURL=meshmakers-octo-meshboard.mjs.map
|