@meshmakers/octo-meshboard 3.5.520 → 3.5.550
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) {
|
|
@@ -4350,6 +4371,49 @@ class QueryExecutorService {
|
|
|
4350
4371
|
};
|
|
4351
4372
|
}));
|
|
4352
4373
|
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Column metadata of a persisted stream-data query for the widget config pickers.
|
|
4376
|
+
* Runs the query with `first: 1` and its own window first; a query that cannot run
|
|
4377
|
+
* without a window — an `Integral` column over a raw archive requires from/to
|
|
4378
|
+
* (AB#7921) — fails or returns no columns, so it is run once more with `window`
|
|
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.
|
|
4396
|
+
*/
|
|
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
|
+
}
|
|
4402
|
+
try {
|
|
4403
|
+
const result = await firstValueFrom(this.executeStreamData(queryRtId, { ...options, silentErrors: true }));
|
|
4404
|
+
if (result.columns.length > 0) {
|
|
4405
|
+
return result;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
catch {
|
|
4409
|
+
// Fall through to the windowed retry below.
|
|
4410
|
+
}
|
|
4411
|
+
return firstValueFrom(this.executeStreamData(queryRtId, {
|
|
4412
|
+
...options,
|
|
4413
|
+
silentErrors: false,
|
|
4414
|
+
streamDataArgs: { ...options.streamDataArgs, from: window.from, to: window.to }
|
|
4415
|
+
}));
|
|
4416
|
+
}
|
|
4353
4417
|
/**
|
|
4354
4418
|
* Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
|
|
4355
4419
|
* query for a base archive family, time window and target point count — so a widget can
|
|
@@ -6422,14 +6486,20 @@ const SUPPORTED_ROW_TYPES$1 = [
|
|
|
6422
6486
|
function sanitizeFieldName(fieldName) {
|
|
6423
6487
|
return fieldName.replace(/\./g, '_');
|
|
6424
6488
|
}
|
|
6489
|
+
/**
|
|
6490
|
+
* Trailing aggregation function suffix the engine appends to a wire-form column:
|
|
6491
|
+
* `_count`, `_sum`, `_avg`, `_min`, `_max`, and `_integral` for the stream-data
|
|
6492
|
+
* Integral (energy over the query window, AB#7921).
|
|
6493
|
+
*/
|
|
6494
|
+
const AGGREGATION_FUNCTION_SUFFIX = /_(?:count|sum|avg|min|max|integral)$/i;
|
|
6425
6495
|
/**
|
|
6426
6496
|
* Strips a trailing aggregation function suffix from a path so the base path can
|
|
6427
6497
|
* be compared independently of which aggregation produced it. The engine emits
|
|
6428
|
-
* cell paths like `meterreading_count` / `amountvalue_sum`
|
|
6429
|
-
* grouped aggregation, and stream-data variants.
|
|
6498
|
+
* cell paths like `meterreading_count` / `amountvalue_sum` / `importpower_integral`
|
|
6499
|
+
* for RT aggregation, grouped aggregation, and stream-data variants.
|
|
6430
6500
|
*/
|
|
6431
6501
|
function stripAggregationFunctionSuffix(path) {
|
|
6432
|
-
return path.replace(
|
|
6502
|
+
return path.replace(AGGREGATION_FUNCTION_SUFFIX, '');
|
|
6433
6503
|
}
|
|
6434
6504
|
/**
|
|
6435
6505
|
* Canonical key form: drop dot and underscore separators and lowercase the rest.
|
|
@@ -6443,11 +6513,11 @@ function toCanonicalAttributeKey(s) {
|
|
|
6443
6513
|
}
|
|
6444
6514
|
/**
|
|
6445
6515
|
* Detects whether a path ends with the engine's aggregation function suffix
|
|
6446
|
-
* (`_count`, `_sum`, `_avg`, `_min`, `_max`). Used to decide whether the
|
|
6447
|
-
* back-compat fallback in {@link matchesAttributePath} should fire.
|
|
6516
|
+
* (`_count`, `_sum`, `_avg`, `_min`, `_max`, `_integral`). Used to decide whether the
|
|
6517
|
+
* loose back-compat fallback in {@link matchesAttributePath} should fire.
|
|
6448
6518
|
*/
|
|
6449
6519
|
function hasAggregationFunctionSuffix(path) {
|
|
6450
|
-
return
|
|
6520
|
+
return AGGREGATION_FUNCTION_SUFFIX.test(path);
|
|
6451
6521
|
}
|
|
6452
6522
|
/**
|
|
6453
6523
|
* Returns true when a cell's `attributePath` refers to the same source attribute
|
|
@@ -6701,10 +6771,10 @@ function processPieChartData(rows, categoryField, valueField) {
|
|
|
6701
6771
|
/**
|
|
6702
6772
|
* Applies an optional scale factor to a query value (KPI `valueMultiplier`).
|
|
6703
6773
|
* Only numeric values are scaled — strings (e.g. enum labels or error
|
|
6704
|
-
* placeholders) pass through unchanged, as does everything
|
|
6705
|
-
* is configured. Use case:
|
|
6706
|
-
* (
|
|
6707
|
-
*
|
|
6774
|
+
* placeholders) and `null` (no data) pass through unchanged, as does everything
|
|
6775
|
+
* when no multiplier is configured. Use case: the unit factor of an energy KPI
|
|
6776
|
+
* over a stream-data Integral column (value × hours): 1 for kW → kWh, 0.001 for
|
|
6777
|
+
* W → kWh (AB#7921).
|
|
6708
6778
|
*/
|
|
6709
6779
|
function applyValueMultiplier(value, multiplier) {
|
|
6710
6780
|
if (typeof multiplier !== 'number' || typeof value !== 'number') {
|
|
@@ -7184,8 +7254,8 @@ class KpiWidgetComponent {
|
|
|
7184
7254
|
value = this.extractGroupedAggregationValue(result);
|
|
7185
7255
|
break;
|
|
7186
7256
|
}
|
|
7187
|
-
// Optional
|
|
7188
|
-
// Applied to numeric values only; strings pass through unchanged.
|
|
7257
|
+
// Optional unit factor (e.g. 1 for an Integral over kW → kWh, 0.001 for W → kWh).
|
|
7258
|
+
// Applied to numeric values only; strings and null (no data) pass through unchanged.
|
|
7189
7259
|
value = applyValueMultiplier(value, this.config.valueMultiplier);
|
|
7190
7260
|
// Create a synthetic entity with the value
|
|
7191
7261
|
const kpiEntity = {
|
|
@@ -7254,10 +7324,12 @@ class KpiWidgetComponent {
|
|
|
7254
7324
|
/**
|
|
7255
7325
|
* Extracts a cell value, preserving strings and converting numbers.
|
|
7256
7326
|
* Returns the value as-is if it's a string, or parses it as a number.
|
|
7327
|
+
* A null cell is "no data" (e.g. an Integral over an empty window, AB#7921) and
|
|
7328
|
+
* stays null, so the KPI shows its empty state instead of a misleading 0.
|
|
7257
7329
|
*/
|
|
7258
7330
|
extractCellValue(value) {
|
|
7259
7331
|
if (value === null || value === undefined)
|
|
7260
|
-
return
|
|
7332
|
+
return null;
|
|
7261
7333
|
if (typeof value === 'number')
|
|
7262
7334
|
return value;
|
|
7263
7335
|
if (typeof value === 'string') {
|
|
@@ -7515,6 +7587,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
7515
7587
|
type: Output
|
|
7516
7588
|
}] } });
|
|
7517
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
|
+
|
|
7518
7608
|
/**
|
|
7519
7609
|
* Configuration dialog for KPI widgets.
|
|
7520
7610
|
* Allows selecting data source, value attribute, and display options.
|
|
@@ -8082,9 +8172,11 @@ class KpiConfigDialogComponent {
|
|
|
8082
8172
|
aggregationType: c.aggregationType ?? null
|
|
8083
8173
|
}));
|
|
8084
8174
|
}
|
|
8085
|
-
// Stream-data: execute with a tiny page just to surface columns.
|
|
8086
|
-
|
|
8087
|
-
|
|
8175
|
+
// Stream-data: execute with a tiny page just to surface columns. An Integral
|
|
8176
|
+
// column needs from/to (AB#7921), so the executor retries with the board's
|
|
8177
|
+
// window — or the last 24 hours when the board has no time filter.
|
|
8178
|
+
const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
|
|
8179
|
+
return columns.map(c => ({
|
|
8088
8180
|
attributePath: c.attributePath,
|
|
8089
8181
|
attributeValueType: c.attributeValueType ?? '',
|
|
8090
8182
|
aggregationType: c.aggregationType ?? null
|
|
@@ -8103,7 +8195,8 @@ class KpiConfigDialogComponent {
|
|
|
8103
8195
|
try {
|
|
8104
8196
|
// family may be undefined here — the executor falls back to a lookup.
|
|
8105
8197
|
const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
|
|
8106
|
-
|
|
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()));
|
|
8107
8200
|
const values = new Set();
|
|
8108
8201
|
for (const row of result.rows) {
|
|
8109
8202
|
if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
|
|
@@ -8561,7 +8654,7 @@ class KpiConfigDialogComponent {
|
|
|
8561
8654
|
</button>
|
|
8562
8655
|
</div>
|
|
8563
8656
|
</div>
|
|
8564
|
-
`, 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 });
|
|
8565
8658
|
}
|
|
8566
8659
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
|
|
8567
8660
|
type: Component,
|
|
@@ -11831,7 +11924,8 @@ class TableConfigDialogComponent {
|
|
|
11831
11924
|
/**
|
|
11832
11925
|
* The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
|
|
11833
11926
|
* queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
|
|
11834
|
-
* (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.
|
|
11835
11929
|
*/
|
|
11836
11930
|
loadQueryFields(rtId) {
|
|
11837
11931
|
if (this.queryFieldsLoad?.rtId === rtId) {
|
|
@@ -11852,8 +11946,10 @@ class TableConfigDialogComponent {
|
|
|
11852
11946
|
.map(c => c?.attributePath ?? '')
|
|
11853
11947
|
.filter(path => path !== '');
|
|
11854
11948
|
}
|
|
11855
|
-
|
|
11856
|
-
|
|
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);
|
|
11857
11953
|
}
|
|
11858
11954
|
catch (error) {
|
|
11859
11955
|
console.error('Error loading query columns:', error);
|
|
@@ -12688,9 +12784,16 @@ class GaugeWidgetComponent {
|
|
|
12688
12784
|
value = this.extractGroupedAggregationValue(result);
|
|
12689
12785
|
break;
|
|
12690
12786
|
}
|
|
12691
|
-
// Create a synthetic entity with the value
|
|
12692
12787
|
if (generation !== this.loadGeneration)
|
|
12693
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
|
|
12694
12797
|
const gaugeEntity = {
|
|
12695
12798
|
rtId: 'query-entity',
|
|
12696
12799
|
ckTypeId: 'system.query',
|
|
@@ -12755,7 +12858,10 @@ class GaugeWidgetComponent {
|
|
|
12755
12858
|
}
|
|
12756
12859
|
return 0;
|
|
12757
12860
|
}
|
|
12861
|
+
/** Parses a cell value; a null cell stays null ("no data", AB#7975), unparsable text is 0. */
|
|
12758
12862
|
parseNumericValue(value) {
|
|
12863
|
+
if (value === null || value === undefined)
|
|
12864
|
+
return null;
|
|
12759
12865
|
if (typeof value === 'number')
|
|
12760
12866
|
return value;
|
|
12761
12867
|
const parsed = parseFloat(String(value));
|
|
@@ -13619,8 +13725,10 @@ class GaugeConfigDialogComponent {
|
|
|
13619
13725
|
aggregationType: c.aggregationType ?? null
|
|
13620
13726
|
}));
|
|
13621
13727
|
}
|
|
13622
|
-
|
|
13623
|
-
|
|
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 => ({
|
|
13624
13732
|
attributePath: c.attributePath,
|
|
13625
13733
|
attributeValueType: c.attributeValueType ?? '',
|
|
13626
13734
|
aggregationType: c.aggregationType ?? null
|
|
@@ -13639,7 +13747,8 @@ class GaugeConfigDialogComponent {
|
|
|
13639
13747
|
// family may be undefined — the executor falls back to a lookup.
|
|
13640
13748
|
const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
|
|
13641
13749
|
try {
|
|
13642
|
-
|
|
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()));
|
|
13643
13752
|
const values = new Set();
|
|
13644
13753
|
for (const row of result.rows) {
|
|
13645
13754
|
if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
|
|
@@ -14103,7 +14212,7 @@ class GaugeConfigDialogComponent {
|
|
|
14103
14212
|
</button>
|
|
14104
14213
|
</div>
|
|
14105
14214
|
</div>
|
|
14106
|
-
`, 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 });
|
|
14107
14216
|
}
|
|
14108
14217
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
|
|
14109
14218
|
type: Component,
|
|
@@ -14927,14 +15036,18 @@ class PieChartWidgetComponent {
|
|
|
14927
15036
|
const categoryCell = findCellForField(row.cells, this.config.categoryField);
|
|
14928
15037
|
const valueCell = findCellForField(row.cells, this.config.valueField);
|
|
14929
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);
|
|
14930
15042
|
let value = 0;
|
|
14931
|
-
if (valueCell) {
|
|
15043
|
+
if (valueCell && !noData) {
|
|
14932
15044
|
const numValue = typeof valueCell.value === 'number' ? valueCell.value : parseFloat(String(valueCell.value));
|
|
14933
15045
|
value = isNaN(numValue) ? 0 : numValue;
|
|
14934
15046
|
}
|
|
14935
|
-
return { category, value };
|
|
15047
|
+
return { category, value, noData };
|
|
14936
15048
|
})
|
|
14937
|
-
.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 }));
|
|
14938
15051
|
this._rawData.set(chartData);
|
|
14939
15052
|
this._isLoading.set(false);
|
|
14940
15053
|
}
|
|
@@ -15329,8 +15442,10 @@ class PieChartConfigDialogComponent {
|
|
|
15329
15442
|
aggregationType: c.aggregationType ?? null
|
|
15330
15443
|
}));
|
|
15331
15444
|
}
|
|
15332
|
-
|
|
15333
|
-
|
|
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 => ({
|
|
15334
15449
|
attributePath: c.attributePath,
|
|
15335
15450
|
attributeValueType: c.attributeValueType ?? '',
|
|
15336
15451
|
aggregationType: c.aggregationType ?? null
|
|
@@ -15970,6 +16085,541 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
15970
16085
|
type: Input
|
|
15971
16086
|
}] } });
|
|
15972
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
|
+
|
|
15973
16623
|
const CHART_TYPE_MAPPING = {
|
|
15974
16624
|
column: { type: 'column', stack: false },
|
|
15975
16625
|
bar: { type: 'bar', stack: false },
|
|
@@ -15990,6 +16640,7 @@ class BarChartWidgetComponent {
|
|
|
15990
16640
|
]);
|
|
15991
16641
|
stateService = inject(MeshBoardStateService);
|
|
15992
16642
|
variableService = inject(MeshBoardVariableService);
|
|
16643
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
15993
16644
|
config;
|
|
15994
16645
|
// Widget state signals
|
|
15995
16646
|
_isLoading = signal(false, /* @ts-ignore */
|
|
@@ -16004,6 +16655,13 @@ class BarChartWidgetComponent {
|
|
|
16004
16655
|
categories = this._categories.asReadonly();
|
|
16005
16656
|
seriesData = this._seriesData.asReadonly();
|
|
16006
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();
|
|
16007
16665
|
data = computed(() => this._seriesData(), /* @ts-ignore */
|
|
16008
16666
|
...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
16009
16667
|
/**
|
|
@@ -16089,6 +16747,27 @@ class BarChartWidgetComponent {
|
|
|
16089
16747
|
isDynamicSeriesMode() {
|
|
16090
16748
|
return !!(this.config.seriesGroupField && this.config.valueField);
|
|
16091
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
|
+
}
|
|
16092
16771
|
formatValue(value) {
|
|
16093
16772
|
return value.toLocaleString('de-AT', {
|
|
16094
16773
|
minimumFractionDigits: 0,
|
|
@@ -16100,6 +16779,12 @@ class BarChartWidgetComponent {
|
|
|
16100
16779
|
if (this.isNotConfigured()) {
|
|
16101
16780
|
return;
|
|
16102
16781
|
}
|
|
16782
|
+
if (this.hasAdditionalSeries()) {
|
|
16783
|
+
await this.loadMultiSource();
|
|
16784
|
+
return;
|
|
16785
|
+
}
|
|
16786
|
+
this._multiSource.set(false);
|
|
16787
|
+
this._sourceHints.set([]);
|
|
16103
16788
|
// Note: isNotConfigured() check ensures dataSource and queryRtId are set
|
|
16104
16789
|
const queryDataSource = this.config.dataSource;
|
|
16105
16790
|
this._isLoading.set(true);
|
|
@@ -16143,6 +16828,141 @@ class BarChartWidgetComponent {
|
|
|
16143
16828
|
this._isLoading.set(false);
|
|
16144
16829
|
}
|
|
16145
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
|
+
}
|
|
16146
16966
|
buildStreamDataArgs() {
|
|
16147
16967
|
const ds = this.config.dataSource;
|
|
16148
16968
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
@@ -16173,15 +16993,16 @@ class BarChartWidgetComponent {
|
|
|
16173
16993
|
for (const seriesConfig of (this.config.series ?? [])) {
|
|
16174
16994
|
const seriesCell = findCellForField(row.cells, seriesConfig.field);
|
|
16175
16995
|
if (seriesCell) {
|
|
16176
|
-
|
|
16177
|
-
rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
|
|
16996
|
+
rowValues.set(seriesConfig.field, BarChartWidgetComponent.toBarValue(seriesCell.value));
|
|
16178
16997
|
}
|
|
16179
16998
|
}
|
|
16180
16999
|
if (categoryValue !== '') {
|
|
16181
17000
|
categories.push(categoryValue);
|
|
16182
17001
|
// Add values for each series
|
|
16183
17002
|
for (const seriesConfig of (this.config.series ?? [])) {
|
|
16184
|
-
|
|
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;
|
|
16185
17006
|
seriesMap.get(seriesConfig.field)?.push(value);
|
|
16186
17007
|
}
|
|
16187
17008
|
}
|
|
@@ -16228,13 +17049,7 @@ class BarChartWidgetComponent {
|
|
|
16228
17049
|
const valueCell = findCellForField(row.cells, valueField);
|
|
16229
17050
|
const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
|
|
16230
17051
|
const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
|
|
16231
|
-
|
|
16232
|
-
if (valueCell) {
|
|
16233
|
-
const val = valueCell.value;
|
|
16234
|
-
numericValue = typeof val === 'number' ? val : parseFloat(String(val));
|
|
16235
|
-
if (isNaN(numericValue))
|
|
16236
|
-
numericValue = 0;
|
|
16237
|
-
}
|
|
17052
|
+
const numericValue = valueCell ? BarChartWidgetComponent.toBarValue(valueCell.value) : 0;
|
|
16238
17053
|
if (categoryValue && seriesGroupValue) {
|
|
16239
17054
|
allCategories.add(categoryValue);
|
|
16240
17055
|
allSeriesGroups.add(seriesGroupValue);
|
|
@@ -16251,7 +17066,12 @@ class BarChartWidgetComponent {
|
|
|
16251
17066
|
const valueScale = this.config?.valueScale ?? 1;
|
|
16252
17067
|
const seriesData = seriesGroups.map(seriesGroup => {
|
|
16253
17068
|
const rawData = categories.map(category => {
|
|
16254
|
-
|
|
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;
|
|
16255
17075
|
});
|
|
16256
17076
|
return {
|
|
16257
17077
|
name: seriesGroup,
|
|
@@ -16276,11 +17096,21 @@ class BarChartWidgetComponent {
|
|
|
16276
17096
|
applyColorThresholds(data) {
|
|
16277
17097
|
const thresholds = this.config.colorThresholds ?? [];
|
|
16278
17098
|
const defaultColor = this.config.defaultBarColor ?? '#6b7280';
|
|
16279
|
-
return data.map(val => ({
|
|
17099
|
+
return data.map(val => val === null ? { value: null } : ({
|
|
16280
17100
|
value: val,
|
|
16281
17101
|
_color: this.getColorForValue(val, thresholds, defaultColor)
|
|
16282
17102
|
}));
|
|
16283
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
|
+
}
|
|
16284
17114
|
getColorForValue(val, thresholds, defaultColor) {
|
|
16285
17115
|
for (const t of thresholds) {
|
|
16286
17116
|
if (val <= t.value)
|
|
@@ -16324,6 +17154,15 @@ class BarChartWidgetComponent {
|
|
|
16324
17154
|
<span>{{ error() }}</span>
|
|
16325
17155
|
</div>
|
|
16326
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
|
+
}
|
|
16327
17166
|
<kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
|
|
16328
17167
|
<kendo-chart-area [background]="'transparent'"></kendo-chart-area>
|
|
16329
17168
|
<kendo-chart-category-axis>
|
|
@@ -16376,18 +17215,32 @@ class BarChartWidgetComponent {
|
|
|
16376
17215
|
[labels]="{ color: chartTheme().text }">
|
|
16377
17216
|
</kendo-chart-legend>
|
|
16378
17217
|
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
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
|
+
}
|
|
16387
17240
|
</kendo-chart>
|
|
16388
17241
|
}
|
|
16389
17242
|
</div>
|
|
16390
|
-
`, 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 });
|
|
16391
17244
|
}
|
|
16392
17245
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartWidgetComponent, decorators: [{
|
|
16393
17246
|
type: Component,
|
|
@@ -16408,6 +17261,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
16408
17261
|
<span>{{ error() }}</span>
|
|
16409
17262
|
</div>
|
|
16410
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
|
+
}
|
|
16411
17273
|
<kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
|
|
16412
17274
|
<kendo-chart-area [background]="'transparent'"></kendo-chart-area>
|
|
16413
17275
|
<kendo-chart-category-axis>
|
|
@@ -16460,22 +17322,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
16460
17322
|
[labels]="{ color: chartTheme().text }">
|
|
16461
17323
|
</kendo-chart-legend>
|
|
16462
17324
|
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
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
|
+
}
|
|
16471
17347
|
</kendo-chart>
|
|
16472
17348
|
}
|
|
16473
17349
|
</div>
|
|
16474
|
-
`, 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"] }]
|
|
16475
17351
|
}], propDecorators: { config: [{
|
|
16476
17352
|
type: Input
|
|
16477
17353
|
}] } });
|
|
16478
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
|
+
|
|
16479
17953
|
/**
|
|
16480
17954
|
* Configuration dialog for Bar Chart widgets.
|
|
16481
17955
|
* Supports column, bar, stacked, and 100% stacked chart types.
|
|
@@ -16485,6 +17959,7 @@ class BarChartConfigDialogComponent {
|
|
|
16485
17959
|
queryExecutor = inject(QueryExecutorService);
|
|
16486
17960
|
stateService = inject(MeshBoardStateService);
|
|
16487
17961
|
windowRef = inject(WindowRef);
|
|
17962
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
16488
17963
|
querySelector;
|
|
16489
17964
|
// Initial values for editing
|
|
16490
17965
|
initialQueryRtId;
|
|
@@ -16503,6 +17978,16 @@ class BarChartConfigDialogComponent {
|
|
|
16503
17978
|
initialColorThresholds;
|
|
16504
17979
|
initialDefaultBarColor;
|
|
16505
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
|
+
}
|
|
16506
17991
|
// State
|
|
16507
17992
|
isLoadingInitial = false;
|
|
16508
17993
|
isLoadingColumns = false;
|
|
@@ -16558,6 +18043,10 @@ class BarChartConfigDialogComponent {
|
|
|
16558
18043
|
if (this.selectedPersistentQuery === null || this.form.categoryField === '') {
|
|
16559
18044
|
return false;
|
|
16560
18045
|
}
|
|
18046
|
+
const seriesValid = querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator, { requireCategoryField: true })
|
|
18047
|
+
.every(e => e === null);
|
|
18048
|
+
if (!seriesValid)
|
|
18049
|
+
return false;
|
|
16561
18050
|
if (this.seriesMode === 'dynamic') {
|
|
16562
18051
|
return this.seriesGroupField !== '' && this.valueField !== '';
|
|
16563
18052
|
}
|
|
@@ -16582,6 +18071,8 @@ class BarChartConfigDialogComponent {
|
|
|
16582
18071
|
this.form.showDataLabels = this.initialShowDataLabels ?? false;
|
|
16583
18072
|
this.form.colorThresholds = this.initialColorThresholds ? [...this.initialColorThresholds] : [];
|
|
16584
18073
|
this.form.defaultBarColor = this.initialDefaultBarColor ?? '';
|
|
18074
|
+
this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
|
|
18075
|
+
this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
|
|
16585
18076
|
// Determine series mode from initial values
|
|
16586
18077
|
if (this.initialSeriesGroupField && this.initialValueField) {
|
|
16587
18078
|
this.seriesMode = 'dynamic';
|
|
@@ -16689,8 +18180,10 @@ class BarChartConfigDialogComponent {
|
|
|
16689
18180
|
aggregationType: c.aggregationType ?? null
|
|
16690
18181
|
}));
|
|
16691
18182
|
}
|
|
16692
|
-
|
|
16693
|
-
|
|
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 => ({
|
|
16694
18187
|
attributePath: c.attributePath,
|
|
16695
18188
|
attributeValueType: c.attributeValueType ?? '',
|
|
16696
18189
|
aggregationType: c.aggregationType ?? null
|
|
@@ -16742,7 +18235,9 @@ class BarChartConfigDialogComponent {
|
|
|
16742
18235
|
showDataLabels: this.form.showDataLabels,
|
|
16743
18236
|
colorThresholds: this.form.colorThresholds.length > 0 ? this.form.colorThresholds : undefined,
|
|
16744
18237
|
defaultBarColor: this.form.defaultBarColor || undefined,
|
|
16745
|
-
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
|
|
16746
18241
|
};
|
|
16747
18242
|
// Add dynamic series fields if in dynamic mode
|
|
16748
18243
|
if (this.seriesMode === 'dynamic') {
|
|
@@ -16761,7 +18256,7 @@ class BarChartConfigDialogComponent {
|
|
|
16761
18256
|
this.windowRef.close();
|
|
16762
18257
|
}
|
|
16763
18258
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16764
|
-
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: `
|
|
16765
18260
|
<div class="config-container">
|
|
16766
18261
|
|
|
16767
18262
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -16900,6 +18395,23 @@ class BarChartConfigDialogComponent {
|
|
|
16900
18395
|
</div>
|
|
16901
18396
|
}
|
|
16902
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
|
+
|
|
16903
18415
|
<!-- Filters Section -->
|
|
16904
18416
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
16905
18417
|
<div class="config-section">
|
|
@@ -17033,7 +18545,7 @@ class BarChartConfigDialogComponent {
|
|
|
17033
18545
|
</button>
|
|
17034
18546
|
</div>
|
|
17035
18547
|
</div>
|
|
17036
|
-
`, 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 });
|
|
17037
18549
|
}
|
|
17038
18550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
|
|
17039
18551
|
type: Component,
|
|
@@ -17048,7 +18560,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17048
18560
|
QuerySelectorComponent,
|
|
17049
18561
|
SdTimeFilterToggleComponent,
|
|
17050
18562
|
EntitySelectorScopePickerComponent,
|
|
17051
|
-
LoadingOverlayComponent
|
|
18563
|
+
LoadingOverlayComponent,
|
|
18564
|
+
LineChartSeriesEditorComponent
|
|
17052
18565
|
], template: `
|
|
17053
18566
|
<div class="config-container">
|
|
17054
18567
|
|
|
@@ -17188,6 +18701,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17188
18701
|
</div>
|
|
17189
18702
|
}
|
|
17190
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
|
+
|
|
17191
18721
|
<!-- Filters Section -->
|
|
17192
18722
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
17193
18723
|
<div class="config-section">
|
|
@@ -17357,8 +18887,179 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
17357
18887
|
type: Input
|
|
17358
18888
|
}], initialFilters: [{
|
|
17359
18889
|
type: Input
|
|
18890
|
+
}], initialAdditionalSeries: [{
|
|
18891
|
+
type: Input
|
|
18892
|
+
}], initialPrimarySeries: [{
|
|
18893
|
+
type: Input
|
|
17360
18894
|
}] } });
|
|
17361
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
|
+
|
|
17362
19063
|
/** Series colours so a series' min/max band and its avg line share one hue. */
|
|
17363
19064
|
const SERIES_PALETTE = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
|
|
17364
19065
|
/**
|
|
@@ -17388,6 +19089,7 @@ class LineChartWidgetComponent {
|
|
|
17388
19089
|
]);
|
|
17389
19090
|
stateService = inject(MeshBoardStateService);
|
|
17390
19091
|
variableService = inject(MeshBoardVariableService);
|
|
19092
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
17391
19093
|
config;
|
|
17392
19094
|
// Widget state signals
|
|
17393
19095
|
_isLoading = signal(false, /* @ts-ignore */
|
|
@@ -17425,6 +19127,10 @@ class LineChartWidgetComponent {
|
|
|
17425
19127
|
valueAxes = this._valueAxes.asReadonly();
|
|
17426
19128
|
error = this._error.asReadonly();
|
|
17427
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();
|
|
17428
19134
|
/**
|
|
17429
19135
|
* Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
|
|
17430
19136
|
* `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
|
|
@@ -17567,6 +19273,49 @@ class LineChartWidgetComponent {
|
|
|
17567
19273
|
return this.config?.chartType ?? 'line';
|
|
17568
19274
|
}, /* @ts-ignore */
|
|
17569
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
|
+
}
|
|
17570
19319
|
labelRotation = computed(() => {
|
|
17571
19320
|
const categoryCount = this._categories().length;
|
|
17572
19321
|
return categoryCount > 5 ? -45 : 0;
|
|
@@ -17668,6 +19417,12 @@ class LineChartWidgetComponent {
|
|
|
17668
19417
|
async loadData() {
|
|
17669
19418
|
if (this.isNotConfigured())
|
|
17670
19419
|
return;
|
|
19420
|
+
this._multiSource.set(this.hasAdditionalSeries());
|
|
19421
|
+
if (this.hasAdditionalSeries()) {
|
|
19422
|
+
await this.loadMultiSource();
|
|
19423
|
+
return;
|
|
19424
|
+
}
|
|
19425
|
+
this._sourceHints.set([]);
|
|
17671
19426
|
const queryDataSource = this.config.dataSource;
|
|
17672
19427
|
this._isLoading.set(true);
|
|
17673
19428
|
this._error.set(null);
|
|
@@ -17849,6 +19604,240 @@ class LineChartWidgetComponent {
|
|
|
17849
19604
|
]
|
|
17850
19605
|
};
|
|
17851
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
|
+
}
|
|
17852
19841
|
buildStreamDataArgs() {
|
|
17853
19842
|
const ds = this.config.dataSource;
|
|
17854
19843
|
const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
|
|
@@ -17918,10 +19907,17 @@ class LineChartWidgetComponent {
|
|
|
17918
19907
|
lineValue = this.toNumber(cell.value); // _avg (or an unsuffixed value)
|
|
17919
19908
|
}
|
|
17920
19909
|
else {
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
if (
|
|
17924
|
-
|
|
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
|
+
}
|
|
17925
19921
|
}
|
|
17926
19922
|
}
|
|
17927
19923
|
else if (unitField && matchesAttributePath(path, unitField)) {
|
|
@@ -18061,6 +20057,15 @@ class LineChartWidgetComponent {
|
|
|
18061
20057
|
</span>
|
|
18062
20058
|
}
|
|
18063
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
|
+
}
|
|
18064
20069
|
@if (showResolutionInfo() && resolutionExplanation(); as msg) {
|
|
18065
20070
|
<div class="resolution-info">
|
|
18066
20071
|
<div class="ri-head">
|
|
@@ -18132,13 +20137,15 @@ class LineChartWidgetComponent {
|
|
|
18132
20137
|
</kendo-chart-series-item>
|
|
18133
20138
|
}
|
|
18134
20139
|
<kendo-chart-series-item
|
|
18135
|
-
[type]="chartType()"
|
|
20140
|
+
[type]="series.asLine ? 'line' : chartType()"
|
|
18136
20141
|
[style]="'smooth'"
|
|
18137
20142
|
[data]="series.data"
|
|
18138
20143
|
[name]="series.name"
|
|
18139
20144
|
[axis]="series.axisName ?? ''"
|
|
18140
20145
|
[color]="series.color"
|
|
18141
|
-
[opacity]="0.7"
|
|
20146
|
+
[opacity]="series.asLine ? 1 : 0.7"
|
|
20147
|
+
[stack]="series.stacked ?? stackSeries()"
|
|
20148
|
+
[missingValues]="missingValues()"
|
|
18142
20149
|
[markers]="{ visible: config.showMarkers ?? false }">
|
|
18143
20150
|
</kendo-chart-series-item>
|
|
18144
20151
|
}
|
|
@@ -18161,9 +20168,16 @@ class LineChartWidgetComponent {
|
|
|
18161
20168
|
<ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
|
|
18162
20169
|
<div class="chart-tooltip">
|
|
18163
20170
|
<strong>{{ category }}</strong>
|
|
18164
|
-
@
|
|
18165
|
-
|
|
18166
|
-
|
|
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
|
+
}
|
|
18167
20181
|
}
|
|
18168
20182
|
}
|
|
18169
20183
|
</div>
|
|
@@ -18172,7 +20186,7 @@ class LineChartWidgetComponent {
|
|
|
18172
20186
|
</kendo-chart>
|
|
18173
20187
|
}
|
|
18174
20188
|
</div>
|
|
18175
|
-
`, 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 });
|
|
18176
20190
|
}
|
|
18177
20191
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
|
|
18178
20192
|
type: Component,
|
|
@@ -18202,6 +20216,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18202
20216
|
</span>
|
|
18203
20217
|
}
|
|
18204
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
|
+
}
|
|
18205
20228
|
@if (showResolutionInfo() && resolutionExplanation(); as msg) {
|
|
18206
20229
|
<div class="resolution-info">
|
|
18207
20230
|
<div class="ri-head">
|
|
@@ -18273,13 +20296,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18273
20296
|
</kendo-chart-series-item>
|
|
18274
20297
|
}
|
|
18275
20298
|
<kendo-chart-series-item
|
|
18276
|
-
[type]="chartType()"
|
|
20299
|
+
[type]="series.asLine ? 'line' : chartType()"
|
|
18277
20300
|
[style]="'smooth'"
|
|
18278
20301
|
[data]="series.data"
|
|
18279
20302
|
[name]="series.name"
|
|
18280
20303
|
[axis]="series.axisName ?? ''"
|
|
18281
20304
|
[color]="series.color"
|
|
18282
|
-
[opacity]="0.7"
|
|
20305
|
+
[opacity]="series.asLine ? 1 : 0.7"
|
|
20306
|
+
[stack]="series.stacked ?? stackSeries()"
|
|
20307
|
+
[missingValues]="missingValues()"
|
|
18283
20308
|
[markers]="{ visible: config.showMarkers ?? false }">
|
|
18284
20309
|
</kendo-chart-series-item>
|
|
18285
20310
|
}
|
|
@@ -18302,9 +20327,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18302
20327
|
<ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
|
|
18303
20328
|
<div class="chart-tooltip">
|
|
18304
20329
|
<strong>{{ category }}</strong>
|
|
18305
|
-
@
|
|
18306
|
-
|
|
18307
|
-
|
|
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
|
+
}
|
|
18308
20340
|
}
|
|
18309
20341
|
}
|
|
18310
20342
|
</div>
|
|
@@ -18313,7 +20345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18313
20345
|
</kendo-chart>
|
|
18314
20346
|
}
|
|
18315
20347
|
</div>
|
|
18316
|
-
`, 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"] }]
|
|
18317
20349
|
}], propDecorators: { config: [{
|
|
18318
20350
|
type: Input
|
|
18319
20351
|
}] } });
|
|
@@ -18327,6 +20359,7 @@ class LineChartConfigDialogComponent {
|
|
|
18327
20359
|
queryExecutor = inject(QueryExecutorService);
|
|
18328
20360
|
stateService = inject(MeshBoardStateService);
|
|
18329
20361
|
windowRef = inject(WindowRef);
|
|
20362
|
+
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
18330
20363
|
querySelector;
|
|
18331
20364
|
// Initial values for editing
|
|
18332
20365
|
initialQueryRtId;
|
|
@@ -18347,6 +20380,13 @@ class LineChartConfigDialogComponent {
|
|
|
18347
20380
|
initialShowDataBadge;
|
|
18348
20381
|
initialReferenceLines;
|
|
18349
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;
|
|
18350
20390
|
// State
|
|
18351
20391
|
isLoadingInitial = false;
|
|
18352
20392
|
isLoadingColumns = false;
|
|
@@ -18401,13 +20441,19 @@ class LineChartConfigDialogComponent {
|
|
|
18401
20441
|
legendPosition: 'right',
|
|
18402
20442
|
showMarkers: false,
|
|
18403
20443
|
showDataBadge: true,
|
|
20444
|
+
stacked: false,
|
|
18404
20445
|
referenceLines: []
|
|
18405
20446
|
};
|
|
18406
20447
|
get isValid() {
|
|
18407
20448
|
return this.selectedPersistentQuery !== null
|
|
18408
20449
|
&& this.form.categoryField !== ''
|
|
18409
20450
|
&& this.form.seriesGroupField !== ''
|
|
18410
|
-
&& 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 : [];
|
|
18411
20457
|
}
|
|
18412
20458
|
async ngOnInit() {
|
|
18413
20459
|
// Initialize filter variables from MeshBoard state
|
|
@@ -18431,6 +20477,9 @@ class LineChartConfigDialogComponent {
|
|
|
18431
20477
|
this.form.showMarkers = this.initialShowMarkers ?? false;
|
|
18432
20478
|
this.form.showDataBadge = this.initialShowDataBadge ?? true;
|
|
18433
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;
|
|
18434
20483
|
// Initialize filters
|
|
18435
20484
|
if (this.initialFilters && this.initialFilters.length > 0) {
|
|
18436
20485
|
this.filters = this.initialFilters.map((f, index) => ({
|
|
@@ -18512,8 +20561,10 @@ class LineChartConfigDialogComponent {
|
|
|
18512
20561
|
aggregationType: c.aggregationType ?? null
|
|
18513
20562
|
}));
|
|
18514
20563
|
}
|
|
18515
|
-
|
|
18516
|
-
|
|
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 => ({
|
|
18517
20568
|
attributePath: c.attributePath,
|
|
18518
20569
|
attributeValueType: c.attributeValueType ?? '',
|
|
18519
20570
|
aggregationType: c.aggregationType ?? null
|
|
@@ -18553,7 +20604,10 @@ class LineChartConfigDialogComponent {
|
|
|
18553
20604
|
showMarkers: this.form.showMarkers,
|
|
18554
20605
|
showDataBadge: this.form.showDataBadge,
|
|
18555
20606
|
referenceLines: this.form.referenceLines.length > 0 ? this.form.referenceLines : undefined,
|
|
18556
|
-
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
|
|
18557
20611
|
};
|
|
18558
20612
|
this.windowRef.close(result);
|
|
18559
20613
|
}
|
|
@@ -18567,7 +20621,7 @@ class LineChartConfigDialogComponent {
|
|
|
18567
20621
|
this.windowRef.close();
|
|
18568
20622
|
}
|
|
18569
20623
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18570
|
-
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: `
|
|
18571
20625
|
<div class="config-container">
|
|
18572
20626
|
|
|
18573
20627
|
<div class="config-form" [class.loading]="isLoadingInitial">
|
|
@@ -18708,6 +20762,22 @@ class LineChartConfigDialogComponent {
|
|
|
18708
20762
|
</div>
|
|
18709
20763
|
}
|
|
18710
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
|
+
|
|
18711
20781
|
<!-- Filters Section -->
|
|
18712
20782
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
18713
20783
|
<div class="config-section">
|
|
@@ -18749,6 +20819,16 @@ class LineChartConfigDialogComponent {
|
|
|
18749
20819
|
</div>
|
|
18750
20820
|
</div>
|
|
18751
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
|
+
|
|
18752
20832
|
<div class="form-row">
|
|
18753
20833
|
<div class="form-field checkbox-field">
|
|
18754
20834
|
<label>
|
|
@@ -18812,7 +20892,7 @@ class LineChartConfigDialogComponent {
|
|
|
18812
20892
|
</button>
|
|
18813
20893
|
</div>
|
|
18814
20894
|
</div>
|
|
18815
|
-
`, 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 });
|
|
18816
20896
|
}
|
|
18817
20897
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
|
|
18818
20898
|
type: Component,
|
|
@@ -18827,7 +20907,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18827
20907
|
QuerySelectorComponent,
|
|
18828
20908
|
SdTimeFilterToggleComponent,
|
|
18829
20909
|
EntitySelectorScopePickerComponent,
|
|
18830
|
-
LoadingOverlayComponent
|
|
20910
|
+
LoadingOverlayComponent,
|
|
20911
|
+
LineChartSeriesEditorComponent
|
|
18831
20912
|
], template: `
|
|
18832
20913
|
<div class="config-container">
|
|
18833
20914
|
|
|
@@ -18969,6 +21050,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
18969
21050
|
</div>
|
|
18970
21051
|
}
|
|
18971
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
|
+
|
|
18972
21069
|
<!-- Filters Section -->
|
|
18973
21070
|
@if (selectedPersistentQuery?.queryCkTypeId) {
|
|
18974
21071
|
<div class="config-section">
|
|
@@ -19010,6 +21107,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19010
21107
|
</div>
|
|
19011
21108
|
</div>
|
|
19012
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
|
+
|
|
19013
21120
|
<div class="form-row">
|
|
19014
21121
|
<div class="form-field checkbox-field">
|
|
19015
21122
|
<label>
|
|
@@ -19113,6 +21220,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
19113
21220
|
type: Input
|
|
19114
21221
|
}], initialFilters: [{
|
|
19115
21222
|
type: Input
|
|
21223
|
+
}], initialAdditionalSeries: [{
|
|
21224
|
+
type: Input
|
|
21225
|
+
}], initialStacked: [{
|
|
21226
|
+
type: Input
|
|
21227
|
+
}], initialPrimarySeries: [{
|
|
21228
|
+
type: Input
|
|
19116
21229
|
}] } });
|
|
19117
21230
|
|
|
19118
21231
|
/**
|
|
@@ -19479,6 +21592,22 @@ class HeatmapWidgetComponent {
|
|
|
19479
21592
|
numericValue = 0;
|
|
19480
21593
|
}
|
|
19481
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
|
+
}
|
|
19482
21611
|
// Stream-data rows carry their time at top level (`row.timestamp`, see
|
|
19483
21612
|
// QueryExecutorService.mapStreamDataRows); the cells only hold the value
|
|
19484
21613
|
// columns. Same source the line chart uses for the bin time (AB#7938).
|
|
@@ -20111,8 +22240,10 @@ class HeatmapConfigDialogComponent {
|
|
|
20111
22240
|
aggregationType: c.aggregationType ?? null
|
|
20112
22241
|
}));
|
|
20113
22242
|
}
|
|
20114
|
-
|
|
20115
|
-
|
|
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 => ({
|
|
20116
22247
|
attributePath: c.attributePath,
|
|
20117
22248
|
attributeValueType: c.attributeValueType ?? '',
|
|
20118
22249
|
aggregationType: c.aggregationType ?? null
|
|
@@ -23139,8 +25270,10 @@ class WidgetGroupConfigDialogComponent {
|
|
|
23139
25270
|
aggregationType: c.aggregationType ?? null
|
|
23140
25271
|
}));
|
|
23141
25272
|
}
|
|
23142
|
-
|
|
23143
|
-
|
|
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 => ({
|
|
23144
25277
|
attributePath: c.attributePath,
|
|
23145
25278
|
attributeValueType: c.attributeValueType ?? '',
|
|
23146
25279
|
aggregationType: c.aggregationType ?? null
|
|
@@ -25982,6 +28115,332 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
25982
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"] }]
|
|
25983
28116
|
}] });
|
|
25984
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
|
+
|
|
25985
28444
|
class ProcessDataService {
|
|
25986
28445
|
dataService = inject(MeshBoardDataService);
|
|
25987
28446
|
variableService = inject(MeshBoardVariableService);
|
|
@@ -26447,51 +28906,90 @@ class ProcessDataService {
|
|
|
26447
28906
|
// Widget Data Binding Methods
|
|
26448
28907
|
// ============================================================================
|
|
26449
28908
|
/**
|
|
26450
|
-
* Loads
|
|
26451
|
-
*
|
|
28909
|
+
* Loads data based on the widget's legacy single-binding configuration —
|
|
28910
|
+
* the implicit binding named `default` (see {@link legacyProcessBinding}).
|
|
26452
28911
|
*
|
|
26453
28912
|
* @param config - The process widget configuration with data binding settings
|
|
26454
28913
|
* @returns BoundDataResult containing the loaded data, or null if no binding configured
|
|
26455
28914
|
*/
|
|
26456
28915
|
async loadBoundData(config) {
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
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]);
|
|
26463
28933
|
}
|
|
26464
|
-
|
|
26465
|
-
|
|
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);
|
|
26466
28946
|
}
|
|
26467
|
-
|
|
28947
|
+
const result = binding.mode === 'runtimeEntity'
|
|
28948
|
+
? await this.loadBoundRuntimeEntityData(binding)
|
|
28949
|
+
: await this.loadBoundQueryData(binding);
|
|
28950
|
+
return { ...result, bindingName: binding.name };
|
|
26468
28951
|
}
|
|
26469
28952
|
catch (error) {
|
|
26470
|
-
console.error(
|
|
28953
|
+
console.error(`[ProcessDataService] Error loading binding '${binding.name}':`, error);
|
|
26471
28954
|
return {
|
|
26472
|
-
type
|
|
28955
|
+
type,
|
|
28956
|
+
bindingName: binding.name,
|
|
26473
28957
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
26474
28958
|
loadedAt: new Date()
|
|
26475
28959
|
};
|
|
26476
28960
|
}
|
|
26477
28961
|
}
|
|
26478
28962
|
/**
|
|
26479
|
-
* 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).
|
|
26480
28968
|
*/
|
|
26481
|
-
async loadBoundRuntimeEntityData(
|
|
26482
|
-
|
|
26483
|
-
if (
|
|
26484
|
-
|
|
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);
|
|
26485
28974
|
}
|
|
26486
|
-
|
|
26487
|
-
|
|
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');
|
|
26488
28988
|
}
|
|
26489
|
-
// Resolve variables in rtId (e.g., ${selectedEntity})
|
|
26490
|
-
const resolvedRtId = this.resolveVariables(bindingRtId);
|
|
26491
28989
|
// Fetch the entity
|
|
26492
|
-
const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(
|
|
28990
|
+
const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(rtId, ckTypeId));
|
|
26493
28991
|
if (!entity) {
|
|
26494
|
-
throw new Error(`Entity not found: ${
|
|
28992
|
+
throw new Error(`Entity not found: ${rtId}`);
|
|
26495
28993
|
}
|
|
26496
28994
|
return {
|
|
26497
28995
|
type: 'entity',
|
|
@@ -26499,39 +28997,50 @@ class ProcessDataService {
|
|
|
26499
28997
|
loadedAt: new Date()
|
|
26500
28998
|
};
|
|
26501
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
|
+
}
|
|
26502
29011
|
/**
|
|
26503
29012
|
* Loads data from a persistent query binding
|
|
26504
29013
|
*/
|
|
26505
|
-
async loadBoundQueryData(
|
|
26506
|
-
const {
|
|
26507
|
-
if (!
|
|
29014
|
+
async loadBoundQueryData(binding) {
|
|
29015
|
+
const { queryRtId, filters, queryFamily } = binding;
|
|
29016
|
+
if (!queryRtId) {
|
|
26508
29017
|
throw new Error('Persistent query binding requires a Query rtId');
|
|
26509
29018
|
}
|
|
26510
29019
|
// Entity-selector scope (AB#7721): NO query is sent while the scope is pending
|
|
26511
29020
|
// or empty — never the persisted (unscoped) scope. Pending → a `pending` result
|
|
26512
29021
|
// (the widget stays loading; a refresh follows once the scope resolves);
|
|
26513
29022
|
// empty → no rows.
|
|
26514
|
-
const scope = this.stateService.resolveStreamDataScope(
|
|
29023
|
+
const scope = this.stateService.resolveStreamDataScope(binding.entitySelectorId, queryFamily, binding.ignoreTimeFilter);
|
|
26515
29024
|
if (scope.kind === 'pending') {
|
|
26516
29025
|
return { type: 'query', pending: true, loadedAt: new Date() };
|
|
26517
29026
|
}
|
|
26518
29027
|
if (scope.kind === 'empty') {
|
|
26519
29028
|
return {
|
|
26520
29029
|
type: 'query',
|
|
26521
|
-
queryResult: this.mapExecutionResult(emptyQueryExecutionResult(
|
|
29030
|
+
queryResult: this.mapExecutionResult(emptyQueryExecutionResult(queryFamily)),
|
|
26522
29031
|
loadedAt: new Date()
|
|
26523
29032
|
};
|
|
26524
29033
|
}
|
|
26525
29034
|
// Convert widget filters to GraphQL format with variable resolution
|
|
26526
|
-
const fieldFilters = this.convertFiltersToDto(
|
|
29035
|
+
const fieldFilters = this.convertFiltersToDto(filters);
|
|
26527
29036
|
// Execute via the unified executor — it routes runtime vs stream-data from
|
|
26528
29037
|
// the persisted family (or a one-time lookup when absent). Stream-data
|
|
26529
29038
|
// queries additionally consume the time-filter / asset-scope overrides built
|
|
26530
29039
|
// below; runtime queries ignore `streamDataArgs`.
|
|
26531
|
-
const result = await firstValueFrom(this.queryExecutor.execute(
|
|
29040
|
+
const result = await firstValueFrom(this.queryExecutor.execute(queryFamily, queryRtId, {
|
|
26532
29041
|
first: 1000, // Reasonable limit for process diagram data
|
|
26533
29042
|
fieldFilter: fieldFilters,
|
|
26534
|
-
streamDataArgs: this.buildBindingStreamDataArgs(
|
|
29043
|
+
streamDataArgs: this.buildBindingStreamDataArgs(binding),
|
|
26535
29044
|
// Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
|
|
26536
29045
|
scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
|
|
26537
29046
|
}));
|
|
@@ -26547,9 +29056,9 @@ class ProcessDataService {
|
|
|
26547
29056
|
* Returns `undefined` when neither override applies (e.g. runtime query, no
|
|
26548
29057
|
* time filter, opted out). Runtime queries ignore the result.
|
|
26549
29058
|
*/
|
|
26550
|
-
buildBindingStreamDataArgs(
|
|
26551
|
-
const timeArgs = this.stateService.resolveStreamDataTimeArgs(
|
|
26552
|
-
const rtIds = this.stateService.resolveStreamDataRtIds(
|
|
29059
|
+
buildBindingStreamDataArgs(binding) {
|
|
29060
|
+
const timeArgs = this.stateService.resolveStreamDataTimeArgs(binding.ignoreTimeFilter);
|
|
29061
|
+
const rtIds = this.stateService.resolveStreamDataRtIds(binding.entitySelectorId);
|
|
26553
29062
|
if (!timeArgs && !rtIds) {
|
|
26554
29063
|
return undefined;
|
|
26555
29064
|
}
|
|
@@ -26627,9 +29136,12 @@ class ProcessWidgetComponent {
|
|
|
26627
29136
|
config;
|
|
26628
29137
|
containerRef;
|
|
26629
29138
|
dataService = inject(ProcessDataService);
|
|
29139
|
+
stateService = inject(MeshBoardStateService);
|
|
26630
29140
|
symbolLibraryService = inject(SymbolLibraryService);
|
|
26631
29141
|
sanitizer = inject(DomSanitizer);
|
|
26632
29142
|
expressionEvaluator = inject(ExpressionEvaluatorService);
|
|
29143
|
+
/** Host language for `text.value` bindings (AB#8022); no language = de-AT. */
|
|
29144
|
+
hostLanguage = injectMeshBoardLanguage();
|
|
26633
29145
|
// Math reference for template
|
|
26634
29146
|
Math = Math;
|
|
26635
29147
|
// State signals
|
|
@@ -26645,6 +29157,16 @@ class ProcessWidgetComponent {
|
|
|
26645
29157
|
...(ngDevMode ? [{ debugName: "_symbolDefinitions" }] : /* istanbul ignore next */ []));
|
|
26646
29158
|
_boundData = signal(null, /* @ts-ignore */
|
|
26647
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;
|
|
26648
29170
|
isLoading = this._isLoading.asReadonly();
|
|
26649
29171
|
error = this._error.asReadonly();
|
|
26650
29172
|
diagramConfig = this._diagramConfig.asReadonly();
|
|
@@ -26655,6 +29177,30 @@ class ProcessWidgetComponent {
|
|
|
26655
29177
|
* Contains entity or query data that can be used for dynamic values in the diagram.
|
|
26656
29178
|
*/
|
|
26657
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 */ []));
|
|
26658
29204
|
// Computed SVG properties
|
|
26659
29205
|
viewBox = computed(() => {
|
|
26660
29206
|
const diagram = this._diagramConfig();
|
|
@@ -26682,7 +29228,7 @@ class ProcessWidgetComponent {
|
|
|
26682
29228
|
animationEnabledStates = computed(() => {
|
|
26683
29229
|
const states = {};
|
|
26684
29230
|
const diagram = this._diagramConfig();
|
|
26685
|
-
const boundData = this.
|
|
29231
|
+
const boundData = this._boundDataByBinding();
|
|
26686
29232
|
if (!diagram?.propertyBindings || diagram.propertyBindings.length === 0) {
|
|
26687
29233
|
return states;
|
|
26688
29234
|
}
|
|
@@ -26692,7 +29238,7 @@ class ProcessWidgetComponent {
|
|
|
26692
29238
|
return states;
|
|
26693
29239
|
}
|
|
26694
29240
|
// Build property values from bound data
|
|
26695
|
-
const propertyValues = boundData
|
|
29241
|
+
const propertyValues = boundData.size > 0
|
|
26696
29242
|
? this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData)
|
|
26697
29243
|
: {};
|
|
26698
29244
|
// Evaluate each animation binding
|
|
@@ -26720,18 +29266,24 @@ class ProcessWidgetComponent {
|
|
|
26720
29266
|
*/
|
|
26721
29267
|
boundPrimitives = computed(() => {
|
|
26722
29268
|
const diagram = this._diagramConfig();
|
|
26723
|
-
const boundData = this.
|
|
29269
|
+
const boundData = this._boundDataByBinding();
|
|
26724
29270
|
if (!diagram?.primitives) {
|
|
26725
29271
|
return [];
|
|
26726
29272
|
}
|
|
26727
|
-
// If no bindings
|
|
26728
|
-
|
|
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')) {
|
|
26729
29280
|
return diagram.primitives;
|
|
26730
29281
|
}
|
|
26731
29282
|
// Build property values from bound data using mappings
|
|
26732
|
-
const
|
|
29283
|
+
const noValue = new Set();
|
|
29284
|
+
const propertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, noValue);
|
|
26733
29285
|
// Apply bindings to primitives
|
|
26734
|
-
return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues);
|
|
29286
|
+
return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues, diagram.transformProperties, noValue);
|
|
26735
29287
|
}, /* @ts-ignore */
|
|
26736
29288
|
...(ngDevMode ? [{ debugName: "boundPrimitives" }] : /* istanbul ignore next */ []));
|
|
26737
29289
|
ngOnInit() {
|
|
@@ -26749,9 +29301,11 @@ class ProcessWidgetComponent {
|
|
|
26749
29301
|
* Loads the process diagram configuration and data
|
|
26750
29302
|
*/
|
|
26751
29303
|
async loadDiagram() {
|
|
29304
|
+
const generation = ++this.loadGeneration;
|
|
26752
29305
|
this._isLoading.set(true);
|
|
26753
29306
|
this._error.set(null);
|
|
26754
29307
|
this._boundData.set(null);
|
|
29308
|
+
this._boundDataByBinding.set(new Map());
|
|
26755
29309
|
// A bound entity-selector scope that is still resolving keeps the widget loading
|
|
26756
29310
|
// until the refresh that follows the resolution (AB#7721).
|
|
26757
29311
|
let scopePending = false;
|
|
@@ -26761,7 +29315,7 @@ class ProcessWidgetComponent {
|
|
|
26761
29315
|
this._diagramConfig.set(this.config.inlineConfig);
|
|
26762
29316
|
await this.loadSymbolDefinitions(this.config.inlineConfig);
|
|
26763
29317
|
await this.loadRuntimeData();
|
|
26764
|
-
scopePending = await this.loadBoundData();
|
|
29318
|
+
scopePending = await this.loadBoundData(generation);
|
|
26765
29319
|
return;
|
|
26766
29320
|
}
|
|
26767
29321
|
// Load from backend by rtId
|
|
@@ -26770,7 +29324,7 @@ class ProcessWidgetComponent {
|
|
|
26770
29324
|
this._diagramConfig.set(diagram);
|
|
26771
29325
|
await this.loadSymbolDefinitions(diagram);
|
|
26772
29326
|
await this.loadRuntimeData();
|
|
26773
|
-
scopePending = await this.loadBoundData();
|
|
29327
|
+
scopePending = await this.loadBoundData(generation);
|
|
26774
29328
|
return;
|
|
26775
29329
|
}
|
|
26776
29330
|
// No configuration
|
|
@@ -26781,7 +29335,7 @@ class ProcessWidgetComponent {
|
|
|
26781
29335
|
this._error.set('Failed to load process diagram');
|
|
26782
29336
|
}
|
|
26783
29337
|
finally {
|
|
26784
|
-
if (!scopePending) {
|
|
29338
|
+
if (!scopePending && generation === this.loadGeneration) {
|
|
26785
29339
|
this._isLoading.set(false);
|
|
26786
29340
|
}
|
|
26787
29341
|
}
|
|
@@ -26790,17 +29344,34 @@ class ProcessWidgetComponent {
|
|
|
26790
29344
|
* Loads bound data based on widget data binding configuration.
|
|
26791
29345
|
* Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
|
|
26792
29346
|
*/
|
|
26793
|
-
async loadBoundData() {
|
|
26794
|
-
|
|
26795
|
-
if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
|
|
26796
|
-
return false;
|
|
26797
|
-
}
|
|
29347
|
+
async loadBoundData(generation) {
|
|
29348
|
+
this.reportBindingIssues();
|
|
26798
29349
|
try {
|
|
26799
|
-
|
|
26800
|
-
|
|
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) {
|
|
26801
29371
|
return true;
|
|
26802
29372
|
}
|
|
26803
|
-
this.
|
|
29373
|
+
this._boundDataByBinding.set(loaded);
|
|
29374
|
+
this._boundData.set(loaded.get(DEFAULT_PROCESS_BINDING_NAME) ?? null);
|
|
26804
29375
|
}
|
|
26805
29376
|
catch (err) {
|
|
26806
29377
|
console.error('Error loading bound data:', err);
|
|
@@ -26808,6 +29379,22 @@ class ProcessWidgetComponent {
|
|
|
26808
29379
|
}
|
|
26809
29380
|
return false;
|
|
26810
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
|
+
}
|
|
26811
29398
|
/**
|
|
26812
29399
|
* Loads symbol definitions for all symbol instances in the diagram
|
|
26813
29400
|
*/
|
|
@@ -27343,11 +29930,12 @@ class ProcessWidgetComponent {
|
|
|
27343
29930
|
return [];
|
|
27344
29931
|
}
|
|
27345
29932
|
// Get property values for this symbol instance
|
|
27346
|
-
const
|
|
29933
|
+
const noValue = new Set();
|
|
29934
|
+
const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition, noValue);
|
|
27347
29935
|
// If there are bindings, apply them
|
|
27348
29936
|
let primitives;
|
|
27349
29937
|
if (definition.propertyBindings && definition.propertyBindings.length > 0) {
|
|
27350
|
-
primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues);
|
|
29938
|
+
primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues, definition.transformProperties, noValue);
|
|
27351
29939
|
}
|
|
27352
29940
|
else {
|
|
27353
29941
|
primitives = [...definition.primitives];
|
|
@@ -27355,7 +29943,7 @@ class ProcessWidgetComponent {
|
|
|
27355
29943
|
// Add primitives from nested symbol instances (recursively)
|
|
27356
29944
|
const nestedInstances = definition.symbolInstances ?? [];
|
|
27357
29945
|
for (const nestedInst of nestedInstances) {
|
|
27358
|
-
const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues);
|
|
29946
|
+
const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues, noValue);
|
|
27359
29947
|
primitives.push(...nestedPrimitives);
|
|
27360
29948
|
}
|
|
27361
29949
|
return primitives;
|
|
@@ -27363,18 +29951,19 @@ class ProcessWidgetComponent {
|
|
|
27363
29951
|
/**
|
|
27364
29952
|
* Gets primitives for a nested symbol instance, with position offset applied.
|
|
27365
29953
|
*/
|
|
27366
|
-
getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues) {
|
|
29954
|
+
getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues, parentNoValue = new Set()) {
|
|
27367
29955
|
const definitions = this._symbolDefinitions();
|
|
27368
29956
|
const nestedDef = definitions.get(nestedInst.symbolRtId);
|
|
27369
29957
|
if (!nestedDef?.primitives) {
|
|
27370
29958
|
return [];
|
|
27371
29959
|
}
|
|
27372
29960
|
// Calculate property values for the nested instance using "Pass to child property" bindings
|
|
27373
|
-
const
|
|
29961
|
+
const nestedNoValue = new Set();
|
|
29962
|
+
const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue, nestedNoValue);
|
|
27374
29963
|
// Apply bindings to nested primitives
|
|
27375
29964
|
let nestedPrimitives;
|
|
27376
29965
|
if (nestedDef.propertyBindings && nestedDef.propertyBindings.length > 0) {
|
|
27377
|
-
nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues);
|
|
29966
|
+
nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues, nestedDef.transformProperties, nestedNoValue);
|
|
27378
29967
|
}
|
|
27379
29968
|
else {
|
|
27380
29969
|
nestedPrimitives = [...nestedDef.primitives];
|
|
@@ -27392,7 +29981,7 @@ class ProcessWidgetComponent {
|
|
|
27392
29981
|
// Recursively handle nested instances within the nested symbol
|
|
27393
29982
|
const deepNestedInstances = nestedDef.symbolInstances ?? [];
|
|
27394
29983
|
for (const deepNestedInst of deepNestedInstances) {
|
|
27395
|
-
const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues);
|
|
29984
|
+
const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues, nestedNoValue);
|
|
27396
29985
|
// Apply the parent offset to deep nested primitives
|
|
27397
29986
|
const deepOffsetPrimitives = deepPrimitives.map(prim => ({
|
|
27398
29987
|
...prim,
|
|
@@ -27408,7 +29997,7 @@ class ProcessWidgetComponent {
|
|
|
27408
29997
|
/**
|
|
27409
29998
|
* Calculate property values for a nested symbol instance based on "Pass to child property" bindings.
|
|
27410
29999
|
*/
|
|
27411
|
-
calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues) {
|
|
30000
|
+
calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue = new Set(), nestedNoValue) {
|
|
27412
30001
|
const nestedPropertyValues = {};
|
|
27413
30002
|
// Get default values from nested symbol definition
|
|
27414
30003
|
const definitions = this._symbolDefinitions();
|
|
@@ -27427,6 +30016,10 @@ class ProcessWidgetComponent {
|
|
|
27427
30016
|
b.targetType === 'symbolInstance' &&
|
|
27428
30017
|
b.targetId === nestedInst.id) ?? [];
|
|
27429
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
|
+
}
|
|
27430
30023
|
const sourceValue = parentPropertyValues[binding.propertyId];
|
|
27431
30024
|
if (sourceValue !== undefined && binding.targetPropertyId) {
|
|
27432
30025
|
// Evaluate expression if present
|
|
@@ -27511,7 +30104,7 @@ class ProcessWidgetComponent {
|
|
|
27511
30104
|
* Gets property values for a symbol instance.
|
|
27512
30105
|
* Combines default values with any values passed from parent bindings.
|
|
27513
30106
|
*/
|
|
27514
|
-
getSymbolInstancePropertyValues(symbolInstance, definition) {
|
|
30107
|
+
getSymbolInstancePropertyValues(symbolInstance, definition, noValue) {
|
|
27515
30108
|
const values = {};
|
|
27516
30109
|
// Initialize with default values from transform properties
|
|
27517
30110
|
for (const prop of definition.transformProperties ?? []) {
|
|
@@ -27519,16 +30112,26 @@ class ProcessWidgetComponent {
|
|
|
27519
30112
|
}
|
|
27520
30113
|
// Override with values from parent diagram bindings (via 'property' effect type)
|
|
27521
30114
|
const diagram = this._diagramConfig();
|
|
27522
|
-
const boundData = this.
|
|
27523
|
-
if (diagram?.propertyBindings
|
|
30115
|
+
const boundData = this._boundDataByBinding();
|
|
30116
|
+
if (diagram?.propertyBindings) {
|
|
27524
30117
|
// Get parent property values
|
|
27525
|
-
const
|
|
30118
|
+
const parentNoValue = new Set();
|
|
30119
|
+
const parentPropertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, parentNoValue);
|
|
27526
30120
|
// Find bindings that pass values to this symbol instance
|
|
27527
30121
|
const propertyBindings = diagram.propertyBindings.filter(b => b.targetType === 'symbolInstance' &&
|
|
27528
30122
|
b.targetId === symbolInstance.id &&
|
|
27529
30123
|
b.effectType === 'property' &&
|
|
27530
30124
|
b.targetPropertyId);
|
|
27531
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
|
+
}
|
|
27532
30135
|
const parentValue = parentPropertyValues[binding.propertyId];
|
|
27533
30136
|
if (parentValue === undefined) {
|
|
27534
30137
|
continue;
|
|
@@ -27949,7 +30552,7 @@ class ProcessWidgetComponent {
|
|
|
27949
30552
|
* Maps diagram exposed properties to values from entity attributes or query columns.
|
|
27950
30553
|
* Applies optional expressions for value transformation/normalization.
|
|
27951
30554
|
*/
|
|
27952
|
-
buildPropertyValuesFromBoundData(transformProperties, mappings,
|
|
30555
|
+
buildPropertyValuesFromBoundData(transformProperties, mappings, boundDataByBinding, noValue) {
|
|
27953
30556
|
const values = {};
|
|
27954
30557
|
// Initialize with default values from transform properties
|
|
27955
30558
|
for (const prop of transformProperties) {
|
|
@@ -27960,14 +30563,42 @@ class ProcessWidgetComponent {
|
|
|
27960
30563
|
const prop = transformProperties.find(p => p.id === mapping.propertyId);
|
|
27961
30564
|
if (!prop)
|
|
27962
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
|
+
}
|
|
27963
30574
|
let rawValue = undefined;
|
|
27964
30575
|
if (mapping.sourceType === 'attribute' && boundData.type === 'entity' && boundData.entity) {
|
|
27965
30576
|
// Extract value from entity attribute
|
|
27966
30577
|
rawValue = this.extractAttributeValue(boundData.entity, mapping.sourcePath);
|
|
27967
30578
|
}
|
|
27968
30579
|
else if (mapping.sourceType === 'column' && boundData.type === 'query' && boundData.queryResult) {
|
|
27969
|
-
//
|
|
27970
|
-
|
|
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);
|
|
27971
30602
|
}
|
|
27972
30603
|
if (rawValue !== undefined) {
|
|
27973
30604
|
let finalValue = rawValue;
|
|
@@ -27991,6 +30622,11 @@ class ProcessWidgetComponent {
|
|
|
27991
30622
|
console.warn(`[ProcessWidget] Expression error for ${mapping.propertyId}:`, err);
|
|
27992
30623
|
}
|
|
27993
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
|
+
}
|
|
27994
30630
|
// Convert to expected type
|
|
27995
30631
|
values[mapping.propertyId] = this.coerceToType(finalValue, prop.type);
|
|
27996
30632
|
}
|
|
@@ -28041,17 +30677,6 @@ class ProcessWidgetComponent {
|
|
|
28041
30677
|
}
|
|
28042
30678
|
return value;
|
|
28043
30679
|
}
|
|
28044
|
-
/**
|
|
28045
|
-
* Extracts a column value from query results (first row).
|
|
28046
|
-
*/
|
|
28047
|
-
extractQueryColumnValue(queryResult, columnName) {
|
|
28048
|
-
if (!queryResult.rows || queryResult.rows.length === 0)
|
|
28049
|
-
return undefined;
|
|
28050
|
-
const firstRow = queryResult.rows[0];
|
|
28051
|
-
// Try both the column name and sanitized version (dots -> underscores)
|
|
28052
|
-
const value = firstRow.cells.get(columnName) ?? firstRow.cells.get(columnName.replace(/\./g, '_'));
|
|
28053
|
-
return value;
|
|
28054
|
-
}
|
|
28055
30680
|
/**
|
|
28056
30681
|
* Coerces a value to the expected type.
|
|
28057
30682
|
*/
|
|
@@ -28083,7 +30708,7 @@ class ProcessWidgetComponent {
|
|
|
28083
30708
|
* Applies property bindings to primitives.
|
|
28084
30709
|
* Creates clones of primitives with binding effects applied.
|
|
28085
30710
|
*/
|
|
28086
|
-
applyBindingsToPrimitives(primitives, bindings, propertyValues) {
|
|
30711
|
+
applyBindingsToPrimitives(primitives, bindings, propertyValues, properties, noValue) {
|
|
28087
30712
|
return primitives.map(primitive => {
|
|
28088
30713
|
// Find bindings for this primitive
|
|
28089
30714
|
const primitiveBindings = bindings.filter(b => b.targetType === 'primitive' && b.targetId === primitive.id);
|
|
@@ -28094,6 +30719,17 @@ class ProcessWidgetComponent {
|
|
|
28094
30719
|
let modified = this.clonePrimitive(primitive);
|
|
28095
30720
|
for (const binding of primitiveBindings) {
|
|
28096
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
|
+
}
|
|
28097
30733
|
if (propertyValue === undefined)
|
|
28098
30734
|
continue;
|
|
28099
30735
|
// Build expression context
|
|
@@ -29200,8 +31836,14 @@ class ProcessWidgetComponent {
|
|
|
29200
31836
|
<span>No process diagram configured</span>
|
|
29201
31837
|
</div>
|
|
29202
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
|
+
}
|
|
29203
31845
|
</div>
|
|
29204
|
-
`, 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 });
|
|
29205
31847
|
}
|
|
29206
31848
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ProcessWidgetComponent, decorators: [{
|
|
29207
31849
|
type: Component,
|
|
@@ -30201,14 +32843,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
30201
32843
|
<span>No process diagram configured</span>
|
|
30202
32844
|
</div>
|
|
30203
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
|
+
}
|
|
30204
32852
|
</div>
|
|
30205
|
-
`, 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"] }]
|
|
30206
32854
|
}], propDecorators: { config: [{
|
|
30207
32855
|
type: Input
|
|
30208
32856
|
}], containerRef: [{
|
|
30209
32857
|
type: ViewChild,
|
|
30210
32858
|
args: ['containerRef']
|
|
30211
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
|
+
}
|
|
30212
32870
|
|
|
30213
32871
|
/**
|
|
30214
32872
|
* Configuration dialog for Process Widget.
|
|
@@ -31412,6 +34070,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
|
|
|
31412
34070
|
* - Import and call registerProcessWidget() only in routes/components that need it
|
|
31413
34071
|
* - Or use provideProcessWidget() in app config for eager loading
|
|
31414
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
|
+
}
|
|
31415
34106
|
/**
|
|
31416
34107
|
* Registers the Process Widget with the widget registry.
|
|
31417
34108
|
*
|
|
@@ -31467,7 +34158,19 @@ function registerProcessWidget(registry) {
|
|
|
31467
34158
|
initialBindingFilters: widget.bindingFilters,
|
|
31468
34159
|
initialPropertyMappings: widget.propertyMappings
|
|
31469
34160
|
}),
|
|
31470
|
-
|
|
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
|
+
} : ({
|
|
31471
34174
|
...widget,
|
|
31472
34175
|
// Diagram selection
|
|
31473
34176
|
processDiagramRtId: result.processDiagramRtId,
|
|
@@ -31507,35 +34210,42 @@ function registerProcessWidget(registry) {
|
|
|
31507
34210
|
initialZoom: 1
|
|
31508
34211
|
}),
|
|
31509
34212
|
// SOLID: Serialization for persistence
|
|
31510
|
-
toPersistedConfig: (
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
|
|
31514
|
-
|
|
31515
|
-
|
|
31516
|
-
|
|
31517
|
-
|
|
31518
|
-
|
|
31519
|
-
|
|
31520
|
-
|
|
31521
|
-
|
|
31522
|
-
|
|
31523
|
-
|
|
31524
|
-
|
|
31525
|
-
|
|
31526
|
-
|
|
31527
|
-
|
|
31528
|
-
|
|
31529
|
-
|
|
31530
|
-
|
|
31531
|
-
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
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
|
+
},
|
|
31536
34245
|
// SOLID: Deserialization from persistence
|
|
31537
34246
|
fromPersistedConfig: (data, base) => {
|
|
31538
34247
|
const config = parseConfig(data);
|
|
34248
|
+
const bindings = parsePersistedProcessBindings(config['bindings']);
|
|
31539
34249
|
return {
|
|
31540
34250
|
...base,
|
|
31541
34251
|
rtId: data.rtId,
|
|
@@ -31563,7 +34273,9 @@ function registerProcessWidget(registry) {
|
|
|
31563
34273
|
bindingIgnoreTimeFilter: config['bindingIgnoreTimeFilter'],
|
|
31564
34274
|
bindingEntitySelectorId: config['bindingEntitySelectorId'],
|
|
31565
34275
|
bindingFilters: config['bindingFilters'],
|
|
31566
|
-
propertyMappings: config['propertyMappings']
|
|
34276
|
+
propertyMappings: config['propertyMappings'],
|
|
34277
|
+
// Named bindings (AB#8023); absent key stays absent.
|
|
34278
|
+
...(bindings && { bindings })
|
|
31567
34279
|
};
|
|
31568
34280
|
}
|
|
31569
34281
|
});
|
|
@@ -32704,7 +35416,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32704
35416
|
initialShowDataLabels: barWidget.showDataLabels,
|
|
32705
35417
|
initialColorThresholds: barWidget.colorThresholds,
|
|
32706
35418
|
initialDefaultBarColor: barWidget.defaultBarColor,
|
|
32707
|
-
initialFilters: barWidget.filters
|
|
35419
|
+
initialFilters: barWidget.filters,
|
|
35420
|
+
initialAdditionalSeries: barWidget.additionalSeries,
|
|
35421
|
+
initialPrimarySeries: barWidget.primarySeries
|
|
32708
35422
|
};
|
|
32709
35423
|
},
|
|
32710
35424
|
applyConfigResult: (widget, result) => {
|
|
@@ -32735,7 +35449,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32735
35449
|
showDataLabels: result.showDataLabels,
|
|
32736
35450
|
colorThresholds: result.colorThresholds,
|
|
32737
35451
|
defaultBarColor: result.defaultBarColor,
|
|
32738
|
-
filters
|
|
35452
|
+
filters,
|
|
35453
|
+
additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
|
|
35454
|
+
primarySeries: barPrimaryOrUndefined(result.primarySeries)
|
|
32739
35455
|
};
|
|
32740
35456
|
},
|
|
32741
35457
|
// SOLID: Factory function
|
|
@@ -32769,6 +35485,9 @@ function registerDefaultWidgets(registry) {
|
|
|
32769
35485
|
valueScale: widget.valueScale,
|
|
32770
35486
|
colorThresholds: widget.colorThresholds,
|
|
32771
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) }),
|
|
32772
35491
|
queryName: widget.dataSource.queryName,
|
|
32773
35492
|
queryRtId: widget.dataSource.queryRtId,
|
|
32774
35493
|
...(widget.dataSource.queryFamily && {
|
|
@@ -32804,7 +35523,8 @@ function registerDefaultWidgets(registry) {
|
|
|
32804
35523
|
valueScale: config['valueScale'],
|
|
32805
35524
|
colorThresholds: config['colorThresholds'],
|
|
32806
35525
|
defaultBarColor: config['defaultBarColor'],
|
|
32807
|
-
filters: config['filters']
|
|
35526
|
+
filters: config['filters'],
|
|
35527
|
+
...barChartSeriesFromPersisted(config)
|
|
32808
35528
|
};
|
|
32809
35529
|
}
|
|
32810
35530
|
});
|
|
@@ -32840,7 +35560,10 @@ function registerDefaultWidgets(registry) {
|
|
|
32840
35560
|
initialShowMarkers: lineWidget.showMarkers,
|
|
32841
35561
|
initialShowDataBadge: lineWidget.showDataBadge,
|
|
32842
35562
|
initialReferenceLines: lineWidget.referenceLines,
|
|
32843
|
-
initialFilters: lineWidget.filters
|
|
35563
|
+
initialFilters: lineWidget.filters,
|
|
35564
|
+
initialAdditionalSeries: lineWidget.additionalSeries,
|
|
35565
|
+
initialStacked: lineWidget.stacked,
|
|
35566
|
+
initialPrimarySeries: lineWidget.primarySeries
|
|
32844
35567
|
};
|
|
32845
35568
|
},
|
|
32846
35569
|
applyConfigResult: (widget, result) => {
|
|
@@ -32876,7 +35599,10 @@ function registerDefaultWidgets(registry) {
|
|
|
32876
35599
|
showMarkers: result.showMarkers,
|
|
32877
35600
|
showDataBadge: result.showDataBadge,
|
|
32878
35601
|
referenceLines: result.referenceLines,
|
|
32879
|
-
filters
|
|
35602
|
+
filters,
|
|
35603
|
+
additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
|
|
35604
|
+
stacked: result.stacked || undefined,
|
|
35605
|
+
primarySeries: displayOptionsOrUndefined(result.primarySeries)
|
|
32880
35606
|
};
|
|
32881
35607
|
},
|
|
32882
35608
|
createDefaultConfig: (base) => ({
|
|
@@ -32927,7 +35653,12 @@ function registerDefaultWidgets(registry) {
|
|
|
32927
35653
|
...(widget.dataSource.aggregateSeriesByGroup && {
|
|
32928
35654
|
aggregateSeriesByGroup: true
|
|
32929
35655
|
}),
|
|
32930
|
-
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) })
|
|
32931
35662
|
}
|
|
32932
35663
|
}),
|
|
32933
35664
|
fromPersistedConfig: (data, base) => {
|
|
@@ -32948,7 +35679,8 @@ function registerDefaultWidgets(registry) {
|
|
|
32948
35679
|
showMarkers: config['showMarkers'] ?? false,
|
|
32949
35680
|
showDataBadge: config['showDataBadge'] ?? true,
|
|
32950
35681
|
referenceLines: config['referenceLines'],
|
|
32951
|
-
filters: config['filters']
|
|
35682
|
+
filters: config['filters'],
|
|
35683
|
+
...lineChartSeriesFromPersisted(config)
|
|
32952
35684
|
};
|
|
32953
35685
|
}
|
|
32954
35686
|
});
|
|
@@ -33984,6 +36716,72 @@ function provideWidgetRegistrations(registrations) {
|
|
|
33984
36716
|
})
|
|
33985
36717
|
]);
|
|
33986
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
|
+
}
|
|
33987
36785
|
|
|
33988
36786
|
/** True when the selector is configured to follow another selector. */
|
|
33989
36787
|
function isFollowerSelector(selector) {
|
|
@@ -43188,5 +45986,5 @@ function provideCockpitWidgets() {
|
|
|
43188
45986
|
* Generated bundle index. Do not edit.
|
|
43189
45987
|
*/
|
|
43190
45988
|
|
|
43191
|
-
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 };
|
|
43192
45990
|
//# sourceMappingURL=meshmakers-octo-meshboard.mjs.map
|