@meshmakers/octo-meshboard 3.5.530 → 3.5.560

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
1
+ import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, OCTO_SILENT_ERRORS, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
2
2
  export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
3
3
  import * as i0 from '@angular/core';
4
- import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, input, Directive, model, computed, Injector, EnvironmentInjector, ApplicationRef, signal, effect, untracked, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, ChangeDetectorRef, ViewChildren, output, viewChild } 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, 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 variables with <code>{{ variableSyntaxHint }}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
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">Available Variables</span>
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">No variables defined. Add variables in the MeshBoard settings or publish a widget value as output variable.</p>
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 variables with <code>{{ variableSyntaxHint }}</code>. Supported: <code>+ - * / % ^</code>, parentheses,
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">Available Variables</span>
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">No variables defined. Add variables in the MeshBoard settings or publish a widget value as output variable.</p>
1680
+ <p class="field-hint">{{ emptyVariablesText() }}</p>
1661
1681
  }
1662
1682
  </div>
1663
1683
  `, styles: [":host{display:flex;flex-direction:column;gap:20px}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label,.field-label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface)}.required,.field-error{color:var(--kendo-color-error)}.field-hint,.field-error{margin:0;font-size:.8rem}.field-hint,.variable-source{color:var(--kendo-color-subtle)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}\n"] }]
1664
- }], propDecorators: { formula: [{ type: i0.Input, args: [{ isSignal: true, alias: "formula", required: false }] }, { type: i0.Output, args: ["formulaChange"] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], declaredNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "declaredNames", required: false }] }], numericOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "numericOnly", required: false }] }], clampHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "clampHint", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
1684
+ }], propDecorators: { formula: [{ type: i0.Input, args: [{ isSignal: true, alias: "formula", required: false }] }, { type: i0.Output, args: ["formulaChange"] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], declaredNames: [{ type: i0.Input, args: [{ isSignal: true, alias: "declaredNames", required: false }] }], numericOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "numericOnly", required: false }] }], clampHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "clampHint", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], variablesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "variablesLabel", required: false }] }], namesNoun: [{ type: i0.Input, args: [{ isSignal: true, alias: "namesNoun", required: false }] }], syntaxHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "syntaxHint", required: false }] }], emptyVariablesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyVariablesText", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
1665
1685
 
1666
1686
  const GetDashboardsDocumentDto = gql `
1667
1687
  query getDashboards($first: Int) {
@@ -4332,7 +4352,8 @@ class QueryExecutorService {
4332
4352
  fieldFilter: options.fieldFilter ?? undefined,
4333
4353
  arg
4334
4354
  },
4335
- fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first'
4355
+ fetchPolicy: options.forceRefresh ? 'network-only' : 'cache-first',
4356
+ ...(options.silentErrors ? { context: { [OCTO_SILENT_ERRORS]: true } } : {})
4336
4357
  }).pipe(map$1(result => {
4337
4358
  const queryItem = result.data?.streamData?.streamDataQuery?.items?.[0];
4338
4359
  if (!queryItem) {
@@ -4355,23 +4376,43 @@ class QueryExecutorService {
4355
4376
  * Runs the query with `first: 1` and its own window first; a query that cannot run
4356
4377
  * without a window — an `Integral` column over a raw archive requires from/to
4357
4378
  * (AB#7921) — fails or returns no columns, so it is run once more with `window`
4358
- * (the board's time window or a recent default). The error of the retry propagates.
4379
+ * (the board's time window or a recent default, see `columnProbeWindow`). The error of
4380
+ * the retry propagates. `options.forceRefresh` bypasses the Apollo cache for both calls.
4381
+ *
4382
+ * The first, window-less attempt is a probe: its error is expected for such queries
4383
+ * and is not shown as an error toast (AB#7985). Only the retry's error reaches the
4384
+ * global error handler.
4385
+ */
4386
+ async fetchStreamDataColumns(queryRtId, window, options = {}) {
4387
+ const result = await this.executeWithProbeWindow('streamData', queryRtId, { first: 1, forceRefresh: options.forceRefresh ?? false }, window);
4388
+ return result.columns;
4389
+ }
4390
+ /**
4391
+ * Runs a persisted query for a config picker (column list, category values, AB#7975).
4392
+ * Runtime queries run once as {@link execute}. A stream-data query runs with its own
4393
+ * window first — silently, its refusal is expected for an Integral query without from/to
4394
+ * (AB#7985) — and, when that fails or returns no columns, once more with `window`
4395
+ * (`columnProbeWindow`). The error of the retry propagates and is shown.
4359
4396
  */
4360
- async fetchStreamDataColumns(queryRtId, window) {
4397
+ async executeWithProbeWindow(family, queryRtId, options, window) {
4398
+ const resolved = family ?? await this.resolveFamily(queryRtId);
4399
+ if (resolved !== 'streamData') {
4400
+ return firstValueFrom(this.executeRuntime(queryRtId, options));
4401
+ }
4361
4402
  try {
4362
- const result = await firstValueFrom(this.executeStreamData(queryRtId, { first: 1 }));
4403
+ const result = await firstValueFrom(this.executeStreamData(queryRtId, { ...options, silentErrors: true }));
4363
4404
  if (result.columns.length > 0) {
4364
- return result.columns;
4405
+ return result;
4365
4406
  }
4366
4407
  }
4367
4408
  catch {
4368
4409
  // Fall through to the windowed retry below.
4369
4410
  }
4370
- const windowed = await firstValueFrom(this.executeStreamData(queryRtId, {
4371
- first: 1,
4372
- streamDataArgs: { from: window.from, to: window.to }
4411
+ return firstValueFrom(this.executeStreamData(queryRtId, {
4412
+ ...options,
4413
+ silentErrors: false,
4414
+ streamDataArgs: { ...options.streamDataArgs, from: window.from, to: window.to }
4373
4415
  }));
4374
- return windowed.columns;
4375
4416
  }
4376
4417
  /**
4377
4418
  * Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
@@ -7546,6 +7587,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
7546
7587
  type: Output
7547
7588
  }] } });
7548
7589
 
7590
+ /**
7591
+ * Time window the widget config dialogs use to list the columns of a persisted
7592
+ * stream-data query (AB#7921, AB#7975).
7593
+ *
7594
+ * A query with an `Integral` (or TimeWeightedAverage / StateDuration) column over a
7595
+ * raw archive only runs with from/to, so {@link QueryExecutorService.fetchStreamDataColumns}
7596
+ * retries with this window: the board's time window when the board has a time filter,
7597
+ * otherwise the last 24 hours. The window only serves to surface the column metadata;
7598
+ * the widget itself always runs with the board window.
7599
+ */
7600
+ const COLUMN_PROBE_FALLBACK_MS = 24 * 60 * 60 * 1000;
7601
+ function columnProbeWindow(boardWindow, now = new Date()) {
7602
+ if (boardWindow) {
7603
+ return boardWindow;
7604
+ }
7605
+ return { from: new Date(now.getTime() - COLUMN_PROBE_FALLBACK_MS), to: now };
7606
+ }
7607
+
7549
7608
  /**
7550
7609
  * Configuration dialog for KPI widgets.
7551
7610
  * Allows selecting data source, value attribute, and display options.
@@ -8116,21 +8175,13 @@ class KpiConfigDialogComponent {
8116
8175
  // Stream-data: execute with a tiny page just to surface columns. An Integral
8117
8176
  // column needs from/to (AB#7921), so the executor retries with the board's
8118
8177
  // window — or the last 24 hours when the board has no time filter.
8119
- const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, this.columnProbeWindow());
8178
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
8120
8179
  return columns.map(c => ({
8121
8180
  attributePath: c.attributePath,
8122
8181
  attributeValueType: c.attributeValueType ?? '',
8123
8182
  aggregationType: c.aggregationType ?? null
8124
8183
  }));
8125
8184
  }
8126
- columnProbeWindow() {
8127
- const boardWindow = this.meshBoardStateService.resolveStreamDataTimeArgs();
8128
- if (boardWindow) {
8129
- return boardWindow;
8130
- }
8131
- const to = new Date();
8132
- return { from: new Date(to.getTime() - 24 * 60 * 60 * 1000), to };
8133
- }
8134
8185
  async onCategoryFieldChange(categoryField) {
8135
8186
  this.form.queryCategoryField = categoryField;
8136
8187
  this.form.queryCategoryValue = '';
@@ -8144,7 +8195,8 @@ class KpiConfigDialogComponent {
8144
8195
  try {
8145
8196
  // family may be undefined here — the executor falls back to a lookup.
8146
8197
  const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
8147
- const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
8198
+ // A grouped Integral query needs from/to like its column list (AB#7975).
8199
+ const result = await this.queryExecutor.executeWithProbeWindow(family, queryRtId, { first: 100 }, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
8148
8200
  const values = new Set();
8149
8201
  for (const row of result.rows) {
8150
8202
  if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
@@ -8602,7 +8654,7 @@ class KpiConfigDialogComponent {
8602
8654
  </button>
8603
8655
  </div>
8604
8656
  </div>
8605
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
8657
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
8606
8658
  }
8607
8659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
8608
8660
  type: Component,
@@ -11872,7 +11924,8 @@ class TableConfigDialogComponent {
11872
11924
  /**
11873
11925
  * The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
11874
11926
  * queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
11875
- * (no column-introspection endpoint), like the chart dialogs.
11927
+ * (no column-introspection endpoint) through `fetchStreamDataColumns`, which retries with a
11928
+ * time window for queries that need one (Integral, AB#7975), like the chart dialogs.
11876
11929
  */
11877
11930
  loadQueryFields(rtId) {
11878
11931
  if (this.queryFieldsLoad?.rtId === rtId) {
@@ -11893,8 +11946,10 @@ class TableConfigDialogComponent {
11893
11946
  .map(c => c?.attributePath ?? '')
11894
11947
  .filter(path => path !== '');
11895
11948
  }
11896
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1, forceRefresh: true }));
11897
- return sdResult.columns.map(c => c.attributePath).filter(path => !!path);
11949
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
11950
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
11951
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()), { forceRefresh: true });
11952
+ return columns.map(c => c.attributePath).filter(path => !!path);
11898
11953
  }
11899
11954
  catch (error) {
11900
11955
  console.error('Error loading query columns:', error);
@@ -12729,9 +12784,16 @@ class GaugeWidgetComponent {
12729
12784
  value = this.extractGroupedAggregationValue(result);
12730
12785
  break;
12731
12786
  }
12732
- // Create a synthetic entity with the value
12733
12787
  if (generation !== this.loadGeneration)
12734
12788
  return;
12789
+ // A null cell is "no data" (e.g. an Integral over an empty window, AB#7975): the gauge
12790
+ // shows its empty state ("-", pointer at the scale start), not a misleading 0.
12791
+ if (value === null) {
12792
+ this._data.set(null);
12793
+ this._isLoading.set(false);
12794
+ return;
12795
+ }
12796
+ // Create a synthetic entity with the value
12735
12797
  const gaugeEntity = {
12736
12798
  rtId: 'query-entity',
12737
12799
  ckTypeId: 'system.query',
@@ -12796,7 +12858,10 @@ class GaugeWidgetComponent {
12796
12858
  }
12797
12859
  return 0;
12798
12860
  }
12861
+ /** Parses a cell value; a null cell stays null ("no data", AB#7975), unparsable text is 0. */
12799
12862
  parseNumericValue(value) {
12863
+ if (value === null || value === undefined)
12864
+ return null;
12800
12865
  if (typeof value === 'number')
12801
12866
  return value;
12802
12867
  const parsed = parseFloat(String(value));
@@ -13660,8 +13725,10 @@ class GaugeConfigDialogComponent {
13660
13725
  aggregationType: c.aggregationType ?? null
13661
13726
  }));
13662
13727
  }
13663
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
13664
- return sdResult.columns.map(c => ({
13728
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
13729
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
13730
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
13731
+ return columns.map(c => ({
13665
13732
  attributePath: c.attributePath,
13666
13733
  attributeValueType: c.attributeValueType ?? '',
13667
13734
  aggregationType: c.aggregationType ?? null
@@ -13680,7 +13747,8 @@ class GaugeConfigDialogComponent {
13680
13747
  // family may be undefined — the executor falls back to a lookup.
13681
13748
  const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
13682
13749
  try {
13683
- const result = await firstValueFrom(this.queryExecutor.execute(family, queryRtId, { first: 100 }));
13750
+ // A grouped Integral query needs from/to like its column list (AB#7975).
13751
+ const result = await this.queryExecutor.executeWithProbeWindow(family, queryRtId, { first: 100 }, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
13684
13752
  const values = new Set();
13685
13753
  for (const row of result.rows) {
13686
13754
  if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
@@ -14144,7 +14212,7 @@ class GaugeConfigDialogComponent {
14144
14212
  </button>
14145
14213
  </div>
14146
14214
  </div>
14147
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14215
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14148
14216
  }
14149
14217
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
14150
14218
  type: Component,
@@ -14968,14 +15036,18 @@ class PieChartWidgetComponent {
14968
15036
  const categoryCell = findCellForField(row.cells, this.config.categoryField);
14969
15037
  const valueCell = findCellForField(row.cells, this.config.valueField);
14970
15038
  const category = categoryCell ? String(categoryCell.value ?? '') : '';
15039
+ // A null cell is "no data" (e.g. an Integral over an empty window, AB#7975):
15040
+ // the slice is left out instead of being shown as 0.
15041
+ const noData = valueCell !== undefined && (valueCell.value === null || valueCell.value === undefined);
14971
15042
  let value = 0;
14972
- if (valueCell) {
15043
+ if (valueCell && !noData) {
14973
15044
  const numValue = typeof valueCell.value === 'number' ? valueCell.value : parseFloat(String(valueCell.value));
14974
15045
  value = isNaN(numValue) ? 0 : numValue;
14975
15046
  }
14976
- return { category, value };
15047
+ return { category, value, noData };
14977
15048
  })
14978
- .filter(item => item.category !== ''); // Filter out empty categories
15049
+ .filter(item => item.category !== '' && !item.noData) // Filter out empty categories and no-data slices
15050
+ .map(({ category, value }) => ({ category, value }));
14979
15051
  this._rawData.set(chartData);
14980
15052
  this._isLoading.set(false);
14981
15053
  }
@@ -15370,8 +15442,10 @@ class PieChartConfigDialogComponent {
15370
15442
  aggregationType: c.aggregationType ?? null
15371
15443
  }));
15372
15444
  }
15373
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
15374
- return sdResult.columns.map(c => ({
15445
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
15446
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
15447
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
15448
+ return columns.map(c => ({
15375
15449
  attributePath: c.attributePath,
15376
15450
  attributeValueType: c.attributeValueType ?? '',
15377
15451
  aggregationType: c.aggregationType ?? null
@@ -16011,6 +16085,541 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
16011
16085
  type: Input
16012
16086
  }] } });
16013
16087
 
16088
+ /** Referenced series keys of a formula, distinct, in order of first appearance. */
16089
+ function seriesFormulaOperands(formula) {
16090
+ return rewriteFormula(formula ?? '').referencedNames;
16091
+ }
16092
+ /**
16093
+ * Orders formula series so every series comes after the formula series it references (Kahn's
16094
+ * algorithm, stable in input order). References to non-formula keys (query series) are leaves.
16095
+ * Series in or behind a cycle are left out of `order`; `cyclic` names the ones in the cycle.
16096
+ */
16097
+ function orderSeriesFormulas(formulas) {
16098
+ const keys = new Set(formulas.map(f => f.key));
16099
+ const deps = new Map();
16100
+ for (const f of formulas) {
16101
+ deps.set(f.key, seriesFormulaOperands(f.formula).filter(k => keys.has(k)));
16102
+ }
16103
+ const order = [];
16104
+ const done = new Set();
16105
+ let progress = true;
16106
+ while (progress) {
16107
+ progress = false;
16108
+ for (const f of formulas) {
16109
+ if (done.has(f.key))
16110
+ continue;
16111
+ if ((deps.get(f.key) ?? []).every(d => done.has(d))) {
16112
+ done.add(f.key);
16113
+ order.push(f.key);
16114
+ progress = true;
16115
+ }
16116
+ }
16117
+ }
16118
+ const cyclic = formulas
16119
+ .filter(f => !done.has(f.key) && reaches(f.key, f.key, deps))
16120
+ .map(f => f.key);
16121
+ return { order, cyclic };
16122
+ }
16123
+ /** True when `target` is reachable from the dependencies of `start`. */
16124
+ function reaches(start, target, deps) {
16125
+ const visited = new Set();
16126
+ const stack = [...(deps.get(start) ?? [])];
16127
+ while (stack.length > 0) {
16128
+ const current = stack.pop();
16129
+ if (current === target)
16130
+ return true;
16131
+ if (visited.has(current))
16132
+ continue;
16133
+ visited.add(current);
16134
+ stack.push(...(deps.get(current) ?? []));
16135
+ }
16136
+ return false;
16137
+ }
16138
+ /** Message of a circular reference, naming the series involved. */
16139
+ function circularReferenceMessage(cyclic) {
16140
+ return `Circular reference between series: ${cyclic.join(', ')}`;
16141
+ }
16142
+ /**
16143
+ * Validates every formula series of a chart: syntax and unknown keys exactly like the KPI formula
16144
+ * field (`validateFormula`, series keys as available names), then circular references.
16145
+ * Returns one result per formula, in input order.
16146
+ */
16147
+ function validateSeriesFormulas(formulas, availableKeys, evaluator) {
16148
+ const results = formulas.map(f => validateFormula(f.formula, availableKeys, evaluator));
16149
+ const { cyclic } = orderSeriesFormulas(formulas);
16150
+ if (cyclic.length > 0) {
16151
+ const message = circularReferenceMessage(cyclic);
16152
+ formulas.forEach((f, i) => {
16153
+ if (results[i].valid && cyclic.includes(f.key)) {
16154
+ results[i] = { valid: false, error: message, unknownVariables: [] };
16155
+ }
16156
+ });
16157
+ }
16158
+ return results;
16159
+ }
16160
+ /**
16161
+ * Evaluates the formula series bucket by bucket over `operands` (key → values, all of `length`).
16162
+ * Formula series are evaluated in dependency order and their results become operands of later
16163
+ * ones. A formula in a cycle, or with an operand key that has no values, is all gaps.
16164
+ * Returns key → values for every formula series.
16165
+ */
16166
+ function evaluateSeriesFormulas(formulas, operands, length, evaluator) {
16167
+ const values = new Map(operands);
16168
+ const result = new Map();
16169
+ const byKey = new Map(formulas.map(f => [f.key, f]));
16170
+ const { order } = orderSeriesFormulas(formulas);
16171
+ for (const key of order) {
16172
+ const formula = byKey.get(key);
16173
+ const data = evaluateOneSeries(formula.formula, values, length, evaluator);
16174
+ result.set(key, data);
16175
+ values.set(key, data);
16176
+ }
16177
+ for (const f of formulas) {
16178
+ if (!result.has(f.key))
16179
+ result.set(f.key, new Array(length).fill(null));
16180
+ }
16181
+ return result;
16182
+ }
16183
+ function evaluateOneSeries(formula, values, length, evaluator) {
16184
+ const out = new Array(length).fill(null);
16185
+ if (!formula?.trim())
16186
+ return out;
16187
+ const { expression, referencedNames } = rewriteFormula(formula);
16188
+ const columns = referencedNames.map(name => values.get(name));
16189
+ if (columns.some(c => c === undefined))
16190
+ return out;
16191
+ // Identifiers of the operands, as rewriteFormula produced them (`${a}` alone rewrites to the identifier of `a`).
16192
+ const identifiers = referencedNames.map(name => rewriteFormula(`\${${name}}`).expression);
16193
+ for (let i = 0; i < length; i++) {
16194
+ const context = {};
16195
+ let gap = false;
16196
+ for (let j = 0; j < columns.length; j++) {
16197
+ const v = columns[j][i];
16198
+ if (v === null || v === undefined || !Number.isFinite(v)) {
16199
+ gap = true;
16200
+ break;
16201
+ }
16202
+ context[identifiers[j]] = v;
16203
+ }
16204
+ if (gap)
16205
+ continue;
16206
+ const r = evaluator.evaluate(expression, context);
16207
+ if (r.success && typeof r.value === 'number' && Number.isFinite(r.value)) {
16208
+ out[i] = r.value;
16209
+ }
16210
+ }
16211
+ return out;
16212
+ }
16213
+
16214
+ /** Suffix of the legend name of a formula series without any valid bucket. */
16215
+ const NO_DATA_LEGEND_SUFFIX = ' (no data)';
16216
+ /** Source key of the primary data source; series keys cannot clash with it (they start with a letter or `_`, never `__primary`). */
16217
+ const PRIMARY_SOURCE_KEY = '__primary';
16218
+ /** Valid series key: letters, digits and `_`, starting with a letter or `_`. */
16219
+ const SERIES_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
16220
+ /** A query series the widget can load: key, query and value field set. */
16221
+ function isCompleteQuerySeries(def) {
16222
+ return def.kind === 'query'
16223
+ && SERIES_KEY_PATTERN.test(def.key ?? '')
16224
+ && def.key !== PRIMARY_SOURCE_KEY
16225
+ && !!def.dataSource?.queryRtId
16226
+ && !!def.valueField;
16227
+ }
16228
+ /** A formula series the widget can evaluate: valid key and a non-empty formula (validity is checked by the dialog). */
16229
+ function isCompleteFormulaSeries(def) {
16230
+ return def.kind === 'formula'
16231
+ && SERIES_KEY_PATTERN.test(def.key ?? '')
16232
+ && def.key !== PRIMARY_SOURCE_KEY
16233
+ && !!def.formula?.trim();
16234
+ }
16235
+ /** Display name of a series: its label, else its key. */
16236
+ function querySeriesLabel(def) {
16237
+ return def.label?.trim() || def.key;
16238
+ }
16239
+ function toNumberOrNull(value) {
16240
+ if (value === null || value === undefined)
16241
+ return null;
16242
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
16243
+ return isNaN(n) ? null : n;
16244
+ }
16245
+ /** Bucket start instant of a row: the downsampling bin / stream-data row timestamp, else the given category cell. */
16246
+ function rowInstant(row, categoryField, useTimestamp) {
16247
+ let raw = useTimestamp ? row.timestamp : undefined;
16248
+ if ((raw === undefined || raw === null) && categoryField) {
16249
+ raw = findCellForField(row.cells, categoryField)?.value;
16250
+ }
16251
+ const instant = toInstant$1(raw);
16252
+ return instant ? instant.getTime() : null;
16253
+ }
16254
+ /**
16255
+ * Value of a row for `valueField`. Downsampled rows carry `<field>_avg` / `_min` / `_max`: the
16256
+ * average (or an unsuffixed cell) is the value. Raw rows: the exact cell first (AB#4293 rule).
16257
+ * A null cell is "no data" (null), never 0.
16258
+ */
16259
+ function rowValue(row, valueField, downsampled) {
16260
+ if (downsampled) {
16261
+ const cell = row.cells.find(c => {
16262
+ const lower = c.attributePath.toLowerCase();
16263
+ return lower !== 'timestamp' && !lower.endsWith('_min') && !lower.endsWith('_max')
16264
+ && matchesAttributePath(c.attributePath, valueField);
16265
+ });
16266
+ return toNumberOrNull(cell?.value);
16267
+ }
16268
+ return toNumberOrNull(findCellForField(row.cells, valueField)?.value);
16269
+ }
16270
+ /**
16271
+ * Rows of the primary source → one series per `seriesGroupField` value, exactly the grouping of
16272
+ * the single-query chart (a later row of the same series and bucket wins).
16273
+ */
16274
+ function primaryRowsToSeries(rows, fields, downsampled) {
16275
+ const series = new Map();
16276
+ for (const row of rows) {
16277
+ const instant = rowInstant(row, downsampled ? undefined : fields.categoryField, downsampled);
16278
+ if (instant === null)
16279
+ continue;
16280
+ const group = findCellForField(row.cells, fields.seriesGroupField)?.value;
16281
+ if (group === null || group === undefined || String(group) === '')
16282
+ continue;
16283
+ const name = String(group);
16284
+ let entry = series.get(name);
16285
+ if (!entry) {
16286
+ entry = { points: new Map() };
16287
+ series.set(name, entry);
16288
+ }
16289
+ entry.points.set(instant, rowValue(row, fields.valueField, downsampled));
16290
+ if (fields.unitField) {
16291
+ const unit = findCellForField(row.cells, fields.unitField)?.value;
16292
+ if (unit !== null && unit !== undefined && String(unit) !== '')
16293
+ entry.unit = String(unit);
16294
+ }
16295
+ }
16296
+ return Array.from(series.entries()).map(([name, e]) => ({ name, points: e.points, unit: e.unit }));
16297
+ }
16298
+ /**
16299
+ * Rows of a query series → its one series. Several rows in one bucket (e.g. one per source entity
16300
+ * when the scope holds several) are summed; a bucket whose rows are all null stays null.
16301
+ */
16302
+ function queryRowsToSeries(rows, name, valueField, downsampled) {
16303
+ const points = new Map();
16304
+ for (const row of rows) {
16305
+ const instant = rowInstant(row, 'timestamp', true);
16306
+ if (instant === null)
16307
+ continue;
16308
+ const value = rowValue(row, valueField, downsampled);
16309
+ const prev = points.get(instant);
16310
+ if (prev === undefined || prev === null)
16311
+ points.set(instant, value);
16312
+ else if (value !== null)
16313
+ points.set(instant, prev + value);
16314
+ }
16315
+ return { name, points };
16316
+ }
16317
+ /**
16318
+ * Category labels for aligned instants. A label that repeats (the doubled wall-clock hour of a
16319
+ * DST fall-back day) gets ` (2)`, ` (3)` …, so every bucket keeps its own category.
16320
+ */
16321
+ function uniqueCategoryLabels(instants, format) {
16322
+ const seen = new Map();
16323
+ return instants.map(t => {
16324
+ const label = format(new Date(t));
16325
+ const n = (seen.get(label) ?? 0) + 1;
16326
+ seen.set(label, n);
16327
+ return n === 1 ? label : `${label} (${n})`;
16328
+ });
16329
+ }
16330
+ /**
16331
+ * Validation message of each series row of the config dialog (query and formula rows, AB#7951 /
16332
+ * AB#7952), `null` when the row is valid. Keys must match {@link SERIES_KEY_PATTERN} and be unique
16333
+ * across all rows; query rows need a query and a value field; formula rows are validated like the
16334
+ * KPI formula field (syntax, unknown keys — every series key is an available name) and must not be
16335
+ * part of a circular reference. With `requireCategoryField` (bar chart, AB#7953) query rows also
16336
+ * need a category field.
16337
+ */
16338
+ function querySeriesErrors(series, evaluator, options) {
16339
+ const counts = new Map();
16340
+ for (const s of series) {
16341
+ const key = s.key?.trim() ?? '';
16342
+ if (key)
16343
+ counts.set(key, (counts.get(key) ?? 0) + 1);
16344
+ }
16345
+ const keys = [...counts.keys()];
16346
+ const formulaRows = series
16347
+ .map((s, index) => ({ s, index }))
16348
+ .filter((r) => r.s.kind === 'formula');
16349
+ const formulaResults = evaluator
16350
+ ? validateSeriesFormulas(formulaRows.map(r => ({ key: r.s.key?.trim() ?? '', formula: r.s.formula ?? '' })), keys, evaluator)
16351
+ : [];
16352
+ const formulaErrorByIndex = new Map();
16353
+ formulaRows.forEach((r, i) => {
16354
+ const result = formulaResults[i];
16355
+ formulaErrorByIndex.set(r.index, result && !result.valid ? (result.error ?? 'Invalid formula') : null);
16356
+ });
16357
+ return series.map((s, index) => {
16358
+ const key = s.key?.trim() ?? '';
16359
+ if (!key)
16360
+ return 'Enter a key.';
16361
+ if (!SERIES_KEY_PATTERN.test(key) || key === PRIMARY_SOURCE_KEY) {
16362
+ return 'The key may contain letters, digits and _ and must not start with a digit.';
16363
+ }
16364
+ if ((counts.get(key) ?? 0) > 1)
16365
+ return `The key "${key}" is used by another series.`;
16366
+ if (s.kind === 'formula') {
16367
+ if (!s.formula?.trim())
16368
+ return 'Enter a formula.';
16369
+ return formulaErrorByIndex.get(index) ?? null;
16370
+ }
16371
+ if (!s.dataSource?.queryRtId)
16372
+ return 'Select a stream-data query.';
16373
+ if (options?.requireCategoryField && !s.categoryField)
16374
+ return 'Select a category field.';
16375
+ if (!s.valueField)
16376
+ return 'Select a value field.';
16377
+ if (s.dataSource.resolutionAware && !s.dataSource.requiredAggregation)
16378
+ return 'Select the aggregation.';
16379
+ return null;
16380
+ });
16381
+ }
16382
+ /**
16383
+ * Composes the series the chart draws from the aligned grid (AB#7952):
16384
+ *
16385
+ * 1. Operands = the aligned query series by key (hidden ones included — they went through the
16386
+ * alignment like every other series).
16387
+ * 2. Formula series are evaluated per bucket (`evaluateSeriesFormulas`: dependency order, null
16388
+ * operand or non-finite result = gap).
16389
+ * 3. Order: primary series, then `additionalSeries` in their order; hidden series (`visible:
16390
+ * false`, primary via `primary.visible`) are left out. In a stacked area chart a series with
16391
+ * `stacked: false` is drawn as a line and moved behind the stacked ones, so it lies over the stack.
16392
+ * 4. Hints: a visible formula series that (directly or through other formulas) uses a failed or
16393
+ * misaligned query source gets "depends on <source>, which failed"; a visible formula series without any
16394
+ * valid bucket keeps its legend entry with the suffix " (no data)".
16395
+ */
16396
+ function composeChartSeries(args) {
16397
+ const { gridSeries, length, definitions, primary, stackedChart, sourceProblems, evaluator } = args;
16398
+ const formulas = definitions.filter(isCompleteFormulaSeries);
16399
+ const operands = new Map();
16400
+ for (const s of gridSeries) {
16401
+ if (s.sourceKey !== PRIMARY_SOURCE_KEY)
16402
+ operands.set(s.sourceKey, s.data);
16403
+ }
16404
+ const evaluated = formulas.length > 0
16405
+ ? evaluateSeriesFormulas(formulas, operands, length, evaluator)
16406
+ : new Map();
16407
+ const placement = (options) => {
16408
+ const stacked = stackedChart && options?.stacked !== false;
16409
+ return { stacked, asLine: stackedChart && !stacked };
16410
+ };
16411
+ const series = [];
16412
+ if (primary?.visible !== false) {
16413
+ for (const s of gridSeries) {
16414
+ if (s.sourceKey === PRIMARY_SOURCE_KEY)
16415
+ series.push({ name: s.name, data: s.data, unit: s.unit, ...placement(primary) });
16416
+ }
16417
+ }
16418
+ const hints = [];
16419
+ const formulaByKey = new Map(formulas.map(f => [f.key, f]));
16420
+ for (const def of definitions) {
16421
+ if (def.kind === 'query') {
16422
+ if (def.visible === false || !isCompleteQuerySeries(def))
16423
+ continue;
16424
+ const aligned = gridSeries.find(s => s.sourceKey === def.key);
16425
+ if (aligned)
16426
+ series.push({ name: aligned.name, data: aligned.data, unit: aligned.unit, ...placement(def) });
16427
+ continue;
16428
+ }
16429
+ if (!isCompleteFormulaSeries(def) || def.visible === false)
16430
+ continue;
16431
+ const label = querySeriesLabel(def);
16432
+ const failed = failedSourcesOf(def.key, formulaByKey, sourceProblems);
16433
+ for (const source of failed) {
16434
+ hints.push({
16435
+ kind: 'dependency',
16436
+ sourceLabel: label,
16437
+ text: `${label}: depends on ${source.label}, which ${source.problem === 'misaligned' ? 'is not on the chart\'s time grid' : 'failed'}`
16438
+ });
16439
+ }
16440
+ const data = evaluated.get(def.key) ?? new Array(length).fill(null);
16441
+ const hasData = data.some(v => v !== null);
16442
+ series.push({ name: hasData ? label : `${label}${NO_DATA_LEGEND_SUFFIX}`, data, ...placement(def) });
16443
+ }
16444
+ // Lines over the stack come last, so Kendo draws them on top.
16445
+ return { series: [...series.filter(s => !s.asLine), ...series.filter(s => s.asLine)], hints };
16446
+ }
16447
+ /** Failed or misaligned query sources a formula series depends on, directly or through other formulas. */
16448
+ function failedSourcesOf(key, formulaByKey, sourceProblems) {
16449
+ const out = [];
16450
+ const visited = new Set();
16451
+ const stack = [key];
16452
+ while (stack.length > 0) {
16453
+ const current = stack.pop();
16454
+ if (visited.has(current))
16455
+ continue;
16456
+ visited.add(current);
16457
+ const formula = formulaByKey.get(current);
16458
+ if (formula) {
16459
+ stack.push(...seriesFormulaOperands(formula.formula).reverse());
16460
+ continue;
16461
+ }
16462
+ const problem = sourceProblems.get(current);
16463
+ if (problem && problem.problem !== 'noData')
16464
+ out.push(problem);
16465
+ }
16466
+ return out;
16467
+ }
16468
+
16469
+ /** Category of a raw cell value; `null` for an empty value (the row is skipped, as today). */
16470
+ function categoryOf(raw) {
16471
+ if (raw === null || raw === undefined || String(raw) === '')
16472
+ return null;
16473
+ if (raw instanceof Date || isIsoDateTime(raw)) {
16474
+ const instant = toInstant$1(raw);
16475
+ if (instant)
16476
+ return { key: `t:${instant.getTime()}`, instant: instant.getTime(), raw };
16477
+ }
16478
+ return { key: `s:${String(raw)}`, instant: null, raw };
16479
+ }
16480
+ /**
16481
+ * Raw category value of a row: the cell of `categoryField` (exact match first, AB#4293). A
16482
+ * stream-data row carries its time stamp as row property, not as cell, so `timestamp` falls back
16483
+ * to `row.timestamp`.
16484
+ */
16485
+ function rowCategory(row, categoryField) {
16486
+ let raw = findCellForField(row.cells, categoryField)?.value;
16487
+ if ((raw === undefined || raw === null) && categoryField.toLowerCase() === 'timestamp')
16488
+ raw = row.timestamp;
16489
+ return categoryOf(raw);
16490
+ }
16491
+ /**
16492
+ * Bar value of a cell, as the single-query chart reads it: a number, `null` for a null cell ("no
16493
+ * data", AB#7975) and 0 for text that is not a number.
16494
+ */
16495
+ function toBarValue(value) {
16496
+ if (value === null || value === undefined)
16497
+ return null;
16498
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
16499
+ return isNaN(n) ? 0 : n;
16500
+ }
16501
+ function scaled(value, valueScale) {
16502
+ return value === null ? null : value * valueScale;
16503
+ }
16504
+ function addCategory(list, seen, category) {
16505
+ if (seen.has(category.key))
16506
+ return;
16507
+ seen.add(category.key);
16508
+ list.push(category);
16509
+ }
16510
+ /**
16511
+ * Rows of the primary source → its series, exactly the grouping of the single-query chart:
16512
+ * static mode one series per configured value field (a missing cell is 0), dynamic mode one series
16513
+ * per `seriesGroupField` value (a combination missing in a category of this source is 0). A later
16514
+ * row of the same category wins. `valueScale` is applied to every value.
16515
+ */
16516
+ function primaryRowsToCategorySeries(rows, config, valueScale) {
16517
+ const categories = [];
16518
+ const seen = new Set();
16519
+ if (config.seriesGroupField && config.valueField) {
16520
+ const groups = new Map();
16521
+ for (const row of rows) {
16522
+ const category = rowCategory(row, config.categoryField);
16523
+ const groupCell = findCellForField(row.cells, config.seriesGroupField);
16524
+ const group = groupCell ? String(groupCell.value ?? '') : '';
16525
+ if (!category || !group)
16526
+ continue;
16527
+ addCategory(categories, seen, category);
16528
+ const valueCell = findCellForField(row.cells, config.valueField);
16529
+ const value = valueCell ? toBarValue(valueCell.value) : 0;
16530
+ let values = groups.get(group);
16531
+ if (!values) {
16532
+ values = new Map();
16533
+ groups.set(group, values);
16534
+ }
16535
+ values.set(category.key, scaled(value, valueScale));
16536
+ }
16537
+ const series = Array.from(groups.entries()).map(([name, values]) => {
16538
+ for (const c of categories)
16539
+ if (!values.has(c.key))
16540
+ values.set(c.key, 0);
16541
+ return { name, values };
16542
+ });
16543
+ return { categories, series };
16544
+ }
16545
+ const fields = config.series ?? [];
16546
+ const series = fields.map(f => ({ name: f.name ?? f.field, values: new Map() }));
16547
+ for (const row of rows) {
16548
+ const category = rowCategory(row, config.categoryField);
16549
+ if (!category)
16550
+ continue;
16551
+ addCategory(categories, seen, category);
16552
+ fields.forEach((f, i) => {
16553
+ const cell = findCellForField(row.cells, f.field);
16554
+ series[i].values.set(category.key, cell ? scaled(toBarValue(cell.value), valueScale) : 0);
16555
+ });
16556
+ }
16557
+ return { categories, series };
16558
+ }
16559
+ /**
16560
+ * Rows of a query series → its one series. Several rows in one category (e.g. one per source
16561
+ * entity when the scope holds several) are summed; a category whose rows are all null stays null.
16562
+ * `valueScale` is applied to the result.
16563
+ */
16564
+ function queryRowsToCategorySeries(rows, name, categoryField, valueField, valueScale) {
16565
+ const categories = [];
16566
+ const seen = new Set();
16567
+ const values = new Map();
16568
+ for (const row of rows) {
16569
+ const category = rowCategory(row, categoryField);
16570
+ if (!category)
16571
+ continue;
16572
+ addCategory(categories, seen, category);
16573
+ const cell = findCellForField(row.cells, valueField);
16574
+ const value = cell ? toBarValue(cell.value) : null;
16575
+ const prev = values.get(category.key);
16576
+ if (prev === undefined || prev === null)
16577
+ values.set(category.key, value);
16578
+ else if (value !== null)
16579
+ values.set(category.key, prev + value);
16580
+ }
16581
+ for (const [key, v] of values)
16582
+ values.set(key, scaled(v, valueScale));
16583
+ return { categories, series: { name, values } };
16584
+ }
16585
+ /**
16586
+ * Matches the series of all sources per category value. Categories are the union over all sources:
16587
+ * in order of first appearance (primary source first, then the others in their order), and sorted
16588
+ * chronologically when every category is a date-time. A category a source lacks is `null` for
16589
+ * its series — no bar, never 0.
16590
+ */
16591
+ function alignOnCategories(sources) {
16592
+ const categories = [];
16593
+ const seen = new Set();
16594
+ for (const source of sources) {
16595
+ for (const c of source.categories)
16596
+ addCategory(categories, seen, c);
16597
+ }
16598
+ if (categories.length > 0 && categories.every(c => c.instant !== null)) {
16599
+ categories.sort((a, b) => a.instant - b.instant);
16600
+ }
16601
+ const series = [];
16602
+ for (const source of sources) {
16603
+ for (const s of source.series) {
16604
+ series.push({
16605
+ sourceKey: source.sourceKey,
16606
+ name: s.name,
16607
+ data: categories.map(c => (s.values.has(c.key) ? s.values.get(c.key) : null))
16608
+ });
16609
+ }
16610
+ }
16611
+ return { categories, series };
16612
+ }
16613
+ /** Makes labels unique: a repeated label gets ` (2)`, ` (3)` …, so every category keeps its own slot. */
16614
+ function uniqueLabels(labels) {
16615
+ const seen = new Map();
16616
+ return labels.map(label => {
16617
+ const n = (seen.get(label) ?? 0) + 1;
16618
+ seen.set(label, n);
16619
+ return n === 1 ? label : `${label} (${n})`;
16620
+ });
16621
+ }
16622
+
16014
16623
  const CHART_TYPE_MAPPING = {
16015
16624
  column: { type: 'column', stack: false },
16016
16625
  bar: { type: 'bar', stack: false },
@@ -16031,6 +16640,7 @@ class BarChartWidgetComponent {
16031
16640
  ]);
16032
16641
  stateService = inject(MeshBoardStateService);
16033
16642
  variableService = inject(MeshBoardVariableService);
16643
+ expressionEvaluator = inject(ExpressionEvaluatorService);
16034
16644
  config;
16035
16645
  // Widget state signals
16036
16646
  _isLoading = signal(false, /* @ts-ignore */
@@ -16045,6 +16655,13 @@ class BarChartWidgetComponent {
16045
16655
  categories = this._categories.asReadonly();
16046
16656
  seriesData = this._seriesData.asReadonly();
16047
16657
  error = this._error.asReadonly();
16658
+ // Multi-source path (AB#7953): per-source hints and whether the last load went through it.
16659
+ _sourceHints = signal([], /* @ts-ignore */
16660
+ ...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
16661
+ sourceHints = this._sourceHints.asReadonly();
16662
+ _multiSource = signal(false, /* @ts-ignore */
16663
+ ...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
16664
+ isMultiSource = this._multiSource.asReadonly();
16048
16665
  data = computed(() => this._seriesData(), /* @ts-ignore */
16049
16666
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
16050
16667
  /**
@@ -16130,6 +16747,27 @@ class BarChartWidgetComponent {
16130
16747
  isDynamicSeriesMode() {
16131
16748
  return !!(this.config.seriesGroupField && this.config.valueField);
16132
16749
  }
16750
+ /** Whether the chart reads series from more than its primary query or has formula series (AB#7953). */
16751
+ hasAdditionalSeries() {
16752
+ return (this.config?.additionalSeries ?? []).some(s => this.isLoadableQuerySeries(s) || isCompleteFormulaSeries(s));
16753
+ }
16754
+ isLoadableQuerySeries(s) {
16755
+ return isCompleteQuerySeries(s) && !!s.categoryField;
16756
+ }
16757
+ /**
16758
+ * Shared tooltip rows of the multi-source path (AB#7953): one row per drawn series for the
16759
+ * hovered category; no bar (null) shows "–".
16760
+ */
16761
+ tooltipRows(category) {
16762
+ const index = this._categories().indexOf(category);
16763
+ if (index < 0)
16764
+ return [];
16765
+ return this._seriesData().map(s => {
16766
+ const raw = s.data[index];
16767
+ const value = raw !== null && typeof raw === 'object' ? raw.value : raw;
16768
+ return { name: s.name, text: value === null || value === undefined ? '–' : this.formatValue(value) };
16769
+ });
16770
+ }
16133
16771
  formatValue(value) {
16134
16772
  return value.toLocaleString('de-AT', {
16135
16773
  minimumFractionDigits: 0,
@@ -16141,6 +16779,12 @@ class BarChartWidgetComponent {
16141
16779
  if (this.isNotConfigured()) {
16142
16780
  return;
16143
16781
  }
16782
+ if (this.hasAdditionalSeries()) {
16783
+ await this.loadMultiSource();
16784
+ return;
16785
+ }
16786
+ this._multiSource.set(false);
16787
+ this._sourceHints.set([]);
16144
16788
  // Note: isNotConfigured() check ensures dataSource and queryRtId are set
16145
16789
  const queryDataSource = this.config.dataSource;
16146
16790
  this._isLoading.set(true);
@@ -16184,6 +16828,141 @@ class BarChartWidgetComponent {
16184
16828
  this._isLoading.set(false);
16185
16829
  }
16186
16830
  }
16831
+ /**
16832
+ * Multi-source load (AB#7953): the primary query plus every complete query series, each with its
16833
+ * own scope and time-filter opt-out, matched per category value.
16834
+ *
16835
+ * 1. Scope per source: any `pending` → stay loading, no query; `empty` → no series for that
16836
+ * source and a "No data for <source>" hint.
16837
+ * 2. All sources load in parallel; a failing source gives an error hint, the others render.
16838
+ * 3. `valueScale` is applied to every source value, then {@link alignOnCategories} builds the
16839
+ * union of categories (chronological when all are date-times); a category a source lacks is
16840
+ * no bar for its series.
16841
+ * 4. `composeChartSeries` (shared with the line chart) evaluates formula series per category,
16842
+ * drops hidden series and adds "depends on <source>, which failed" hints.
16843
+ */
16844
+ async loadMultiSource() {
16845
+ const primaryDs = this.config.dataSource;
16846
+ const sources = [
16847
+ { key: PRIMARY_SOURCE_KEY, label: primaryDs.queryName?.trim() || 'Primary query', dataSource: primaryDs, primary: true },
16848
+ ...(this.config.additionalSeries ?? [])
16849
+ .filter((s) => this.isLoadableQuerySeries(s))
16850
+ .map(def => ({
16851
+ key: def.key,
16852
+ label: querySeriesLabel(def),
16853
+ dataSource: def.dataSource,
16854
+ primary: false,
16855
+ categoryField: def.categoryField,
16856
+ valueField: def.valueField
16857
+ }))
16858
+ ];
16859
+ this._isLoading.set(true);
16860
+ this._error.set(null);
16861
+ this._multiSource.set(true);
16862
+ const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
16863
+ if (scopes.some(s => s.kind === 'pending')) {
16864
+ return;
16865
+ }
16866
+ try {
16867
+ const hints = [];
16868
+ const problems = new Map();
16869
+ const noData = (source) => {
16870
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
16871
+ problems.set(source.key, { label: source.label, problem: 'noData' });
16872
+ };
16873
+ const active = [];
16874
+ sources.forEach((source, i) => {
16875
+ const scope = scopes[i];
16876
+ if (scope.kind === 'empty')
16877
+ noData(source);
16878
+ else
16879
+ active.push({ source, rtIds: scope.kind === 'scoped' ? scope.rtIds : undefined });
16880
+ });
16881
+ const fetched = await Promise.allSettled(active.map(a => this.fetchSourceRows(a.source, a.rtIds)));
16882
+ const valueScale = this.config.valueScale ?? 1;
16883
+ const categorySources = [];
16884
+ fetched.forEach((f, i) => {
16885
+ const source = active[i].source;
16886
+ if (f.status === 'rejected') {
16887
+ console.error(`Error loading Bar Chart source "${source.label}":`, f.reason);
16888
+ hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
16889
+ problems.set(source.key, { label: source.label, problem: 'error' });
16890
+ return;
16891
+ }
16892
+ const converted = source.primary
16893
+ ? primaryRowsToCategorySeries(f.value, this.config, valueScale)
16894
+ : (r => ({ categories: r.categories, series: [r.series] }))(queryRowsToCategorySeries(f.value, source.label, source.categoryField, source.valueField, valueScale));
16895
+ if (converted.categories.length === 0 || converted.series.length === 0) {
16896
+ noData(source);
16897
+ return;
16898
+ }
16899
+ categorySources.push({ sourceKey: source.key, sourceLabel: source.label, ...converted });
16900
+ });
16901
+ const aligned = alignOnCategories(categorySources);
16902
+ const composed = composeChartSeries({
16903
+ gridSeries: aligned.series,
16904
+ length: aligned.categories.length,
16905
+ definitions: this.config.additionalSeries ?? [],
16906
+ primary: this.config.primarySeries,
16907
+ stackedChart: false,
16908
+ sourceProblems: problems,
16909
+ evaluator: this.expressionEvaluator
16910
+ });
16911
+ hints.push(...composed.hints);
16912
+ const colors = new Map((this.config.series ?? []).map(s => [s.name ?? s.field, s.color]));
16913
+ this._categories.set(this.categoryLabels(aligned.categories));
16914
+ this._seriesData.set(composed.series.map(s => ({
16915
+ name: s.name,
16916
+ data: this.hasColorThresholds() ? this.applyColorThresholds(s.data) : s.data,
16917
+ color: colors.get(s.name)
16918
+ })));
16919
+ this._sourceHints.set(hints);
16920
+ this._isLoading.set(false);
16921
+ }
16922
+ catch (err) {
16923
+ console.error('Error loading Bar Chart data:', err);
16924
+ this._error.set('Failed to load data');
16925
+ this._sourceHints.set([]);
16926
+ this._isLoading.set(false);
16927
+ }
16928
+ }
16929
+ /** Rows of one source of the multi-source path; widget filters apply to the primary query only. */
16930
+ async fetchSourceRows(source, scopeRtIds) {
16931
+ const ds = source.dataSource;
16932
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
16933
+ const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
16934
+ const streamDataArgs = timeArgs || rtIds ? { ...timeArgs, rtIds } : undefined;
16935
+ const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
16936
+ fieldFilter: source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined,
16937
+ streamDataArgs,
16938
+ scopeRtIds
16939
+ }));
16940
+ return result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
16941
+ }
16942
+ /**
16943
+ * Category labels of the multi-source path: date-times as dates when every category is a
16944
+ * different day (daily bars), else date and time as the single-query chart shows them; other
16945
+ * values as text. Repeated labels get " (2)" so every category keeps its slot.
16946
+ */
16947
+ categoryLabels(categories) {
16948
+ const mode = this.stateService.timeZoneMode();
16949
+ const allInstants = categories.length > 0 && categories.every(c => c.instant !== null);
16950
+ const dayOf = (c) => formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '';
16951
+ const byDay = allInstants && new Set(categories.map(dayOf)).size === categories.length;
16952
+ return uniqueLabels(categories.map(c => {
16953
+ if (byDay)
16954
+ return dayOf(c);
16955
+ if (c.instant !== null) {
16956
+ return formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) ?? String(c.raw);
16957
+ }
16958
+ return String(c.raw ?? '');
16959
+ }));
16960
+ }
16961
+ /** Short message of a failed source for its hint. */
16962
+ errorMessage(reason) {
16963
+ const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
16964
+ return message ? `Failed to load data (${message})` : 'Failed to load data';
16965
+ }
16187
16966
  buildStreamDataArgs() {
16188
16967
  const ds = this.config.dataSource;
16189
16968
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
@@ -16214,15 +16993,16 @@ class BarChartWidgetComponent {
16214
16993
  for (const seriesConfig of (this.config.series ?? [])) {
16215
16994
  const seriesCell = findCellForField(row.cells, seriesConfig.field);
16216
16995
  if (seriesCell) {
16217
- const numValue = typeof seriesCell.value === 'number' ? seriesCell.value : parseFloat(String(seriesCell.value));
16218
- rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
16996
+ rowValues.set(seriesConfig.field, BarChartWidgetComponent.toBarValue(seriesCell.value));
16219
16997
  }
16220
16998
  }
16221
16999
  if (categoryValue !== '') {
16222
17000
  categories.push(categoryValue);
16223
17001
  // Add values for each series
16224
17002
  for (const seriesConfig of (this.config.series ?? [])) {
16225
- const value = rowValues.get(seriesConfig.field) ?? 0;
17003
+ // A null cell (no data, e.g. an Integral over an empty day) stays null: no bar
17004
+ // instead of a 0 bar (AB#7975). A missing cell is still 0.
17005
+ const value = rowValues.has(seriesConfig.field) ? rowValues.get(seriesConfig.field) : 0;
16226
17006
  seriesMap.get(seriesConfig.field)?.push(value);
16227
17007
  }
16228
17008
  }
@@ -16269,13 +17049,7 @@ class BarChartWidgetComponent {
16269
17049
  const valueCell = findCellForField(row.cells, valueField);
16270
17050
  const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
16271
17051
  const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
16272
- let numericValue = 0;
16273
- if (valueCell) {
16274
- const val = valueCell.value;
16275
- numericValue = typeof val === 'number' ? val : parseFloat(String(val));
16276
- if (isNaN(numericValue))
16277
- numericValue = 0;
16278
- }
17052
+ const numericValue = valueCell ? BarChartWidgetComponent.toBarValue(valueCell.value) : 0;
16279
17053
  if (categoryValue && seriesGroupValue) {
16280
17054
  allCategories.add(categoryValue);
16281
17055
  allSeriesGroups.add(seriesGroupValue);
@@ -16292,7 +17066,12 @@ class BarChartWidgetComponent {
16292
17066
  const valueScale = this.config?.valueScale ?? 1;
16293
17067
  const seriesData = seriesGroups.map(seriesGroup => {
16294
17068
  const rawData = categories.map(category => {
16295
- return (dataMap.get(category)?.get(seriesGroup) ?? 0) * valueScale;
17069
+ const cell = dataMap.get(category);
17070
+ // Missing combination → 0; a null cell (no data, AB#7975) → no bar.
17071
+ if (!cell?.has(seriesGroup))
17072
+ return 0;
17073
+ const value = cell.get(seriesGroup);
17074
+ return value === null ? null : value * valueScale;
16296
17075
  });
16297
17076
  return {
16298
17077
  name: seriesGroup,
@@ -16317,11 +17096,21 @@ class BarChartWidgetComponent {
16317
17096
  applyColorThresholds(data) {
16318
17097
  const thresholds = this.config.colorThresholds ?? [];
16319
17098
  const defaultColor = this.config.defaultBarColor ?? '#6b7280';
16320
- return data.map(val => ({
17099
+ return data.map(val => val === null ? { value: null } : ({
16321
17100
  value: val,
16322
17101
  _color: this.getColorForValue(val, thresholds, defaultColor)
16323
17102
  }));
16324
17103
  }
17104
+ /**
17105
+ * Bar value of a cell: a number, `null` for a null cell ("no data" — Kendo draws no
17106
+ * bar, AB#7975), and 0 for text that is not a number.
17107
+ */
17108
+ static toBarValue(value) {
17109
+ if (value === null || value === undefined)
17110
+ return null;
17111
+ const numValue = typeof value === 'number' ? value : parseFloat(String(value));
17112
+ return isNaN(numValue) ? 0 : numValue;
17113
+ }
16325
17114
  getColorForValue(val, thresholds, defaultColor) {
16326
17115
  for (const t of thresholds) {
16327
17116
  if (val <= t.value)
@@ -16365,6 +17154,15 @@ class BarChartWidgetComponent {
16365
17154
  <span>{{ error() }}</span>
16366
17155
  </div>
16367
17156
  } @else {
17157
+ @if (sourceHints().length > 0) {
17158
+ <ul class="source-hints" aria-live="polite">
17159
+ @for (hint of sourceHints(); track hint.text) {
17160
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
17161
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
17162
+ </li>
17163
+ }
17164
+ </ul>
17165
+ }
16368
17166
  <kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
16369
17167
  <kendo-chart-area [background]="'transparent'"></kendo-chart-area>
16370
17168
  <kendo-chart-category-axis>
@@ -16417,18 +17215,32 @@ class BarChartWidgetComponent {
16417
17215
  [labels]="{ color: chartTheme().text }">
16418
17216
  </kendo-chart-legend>
16419
17217
 
16420
- <kendo-chart-tooltip>
16421
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
16422
- <div class="chart-tooltip">
16423
- <strong>{{ category }}</strong><br/>
16424
- {{ series.name }}: {{ formatValue(value) }}
16425
- </div>
16426
- </ng-template>
16427
- </kendo-chart-tooltip>
17218
+ @if (isMultiSource()) {
17219
+ <!-- Multi-source (AB#7953): one tooltip per category with every drawn series; no bar shows "–". -->
17220
+ <kendo-chart-tooltip [shared]="true">
17221
+ <ng-template kendoChartSharedTooltipTemplate let-category="category">
17222
+ <div class="chart-tooltip">
17223
+ <strong>{{ category }}</strong>
17224
+ @for (row of tooltipRows(category); track row.name) {
17225
+ <br/>{{ row.name }}: {{ row.text }}
17226
+ }
17227
+ </div>
17228
+ </ng-template>
17229
+ </kendo-chart-tooltip>
17230
+ } @else {
17231
+ <kendo-chart-tooltip>
17232
+ <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
17233
+ <div class="chart-tooltip">
17234
+ <strong>{{ category }}</strong><br/>
17235
+ {{ series.name }}: {{ formatValue(value) }}
17236
+ </div>
17237
+ </ng-template>
17238
+ </kendo-chart-tooltip>
17239
+ }
16428
17240
  </kendo-chart>
16429
17241
  }
16430
17242
  </div>
16431
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17243
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
16432
17244
  }
16433
17245
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartWidgetComponent, decorators: [{
16434
17246
  type: Component,
@@ -16449,6 +17261,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
16449
17261
  <span>{{ error() }}</span>
16450
17262
  </div>
16451
17263
  } @else {
17264
+ @if (sourceHints().length > 0) {
17265
+ <ul class="source-hints" aria-live="polite">
17266
+ @for (hint of sourceHints(); track hint.text) {
17267
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
17268
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
17269
+ </li>
17270
+ }
17271
+ </ul>
17272
+ }
16452
17273
  <kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
16453
17274
  <kendo-chart-area [background]="'transparent'"></kendo-chart-area>
16454
17275
  <kendo-chart-category-axis>
@@ -16501,22 +17322,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
16501
17322
  [labels]="{ color: chartTheme().text }">
16502
17323
  </kendo-chart-legend>
16503
17324
 
16504
- <kendo-chart-tooltip>
16505
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
16506
- <div class="chart-tooltip">
16507
- <strong>{{ category }}</strong><br/>
16508
- {{ series.name }}: {{ formatValue(value) }}
16509
- </div>
16510
- </ng-template>
16511
- </kendo-chart-tooltip>
17325
+ @if (isMultiSource()) {
17326
+ <!-- Multi-source (AB#7953): one tooltip per category with every drawn series; no bar shows "–". -->
17327
+ <kendo-chart-tooltip [shared]="true">
17328
+ <ng-template kendoChartSharedTooltipTemplate let-category="category">
17329
+ <div class="chart-tooltip">
17330
+ <strong>{{ category }}</strong>
17331
+ @for (row of tooltipRows(category); track row.name) {
17332
+ <br/>{{ row.name }}: {{ row.text }}
17333
+ }
17334
+ </div>
17335
+ </ng-template>
17336
+ </kendo-chart-tooltip>
17337
+ } @else {
17338
+ <kendo-chart-tooltip>
17339
+ <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
17340
+ <div class="chart-tooltip">
17341
+ <strong>{{ category }}</strong><br/>
17342
+ {{ series.name }}: {{ formatValue(value) }}
17343
+ </div>
17344
+ </ng-template>
17345
+ </kendo-chart-tooltip>
17346
+ }
16512
17347
  </kendo-chart>
16513
17348
  }
16514
17349
  </div>
16515
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
17350
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"] }]
16516
17351
  }], propDecorators: { config: [{
16517
17352
  type: Input
16518
17353
  }] } });
16519
17354
 
17355
+ const NUMERIC_TYPES = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
17356
+ /**
17357
+ * Series section of the line chart config dialog (AB#7951, wireframe linked in AB#7951): the
17358
+ * series that come after the primary query's series, in legend / stacking order.
17359
+ *
17360
+ * - Query rows: own stream-data query, value field, entity-selector scope, time-filter opt-out and
17361
+ * resolution-aware routing (AB#7951).
17362
+ * - Formula rows (AB#7952): a formula over the other rows' keys in the shared formula field
17363
+ * (series keys as available names, no preview).
17364
+ * - Every row and the primary row: Visible and Stacked (Stacked only in a stacked area chart;
17365
+ * unchecked = drawn as a line over the stack).
17366
+ *
17367
+ * Rows can be added, removed and moved by keyboard; the focus stays on a moved row's button.
17368
+ *
17369
+ * `alignment="category"` (bar chart, AB#7953): rows are matched per category value, so every query
17370
+ * row gets its own category field; Resolution-aware and Stacked are not shown (the chart type
17371
+ * stacks).
17372
+ */
17373
+ class LineChartSeriesEditorComponent {
17374
+ queryExecutor = inject(QueryExecutorService);
17375
+ stateService = inject(MeshBoardStateService);
17376
+ cdr = inject(ChangeDetectorRef);
17377
+ host = inject(ElementRef);
17378
+ evaluator = inject(ExpressionEvaluatorService);
17379
+ /** The series (query and formula rows), in order. */
17380
+ series = [];
17381
+ /** Display options of the primary query's series (AB#7952). */
17382
+ primary;
17383
+ /** The chart stacks (area + stacked): enables the per-series Stacked checkboxes (AB#7952). */
17384
+ stackedChart = false;
17385
+ /** Emits the primary display options on change; `undefined` when both are default. */
17386
+ primaryChange = new EventEmitter();
17387
+ /** Entity selectors of the board (scope picker). */
17388
+ selectors = [];
17389
+ /**
17390
+ * How the chart matches series (AB#7953): `timeGrid` (line chart, default) or `category` (bar
17391
+ * chart: per-row category field, no Resolution-aware, no Stacked).
17392
+ */
17393
+ alignment = 'timeGrid';
17394
+ /** Category field preset for a new query row in category mode (the primary query's category field). */
17395
+ defaultCategoryField;
17396
+ /** Emits a new array on every change. */
17397
+ seriesChange = new EventEmitter();
17398
+ rowCards;
17399
+ get arrowUpIcon() { return arrowUpIcon; }
17400
+ get arrowDownIcon() { return arrowDownIcon; }
17401
+ get plusIcon() { return plusIcon; }
17402
+ get trashIcon() { return MM_ACTION_ICONS.delete; }
17403
+ streamDataOnly = ['streamData'];
17404
+ stackedDisabledHint = 'Only a stacked area chart stacks series.';
17405
+ keySyntaxHint = '${key}';
17406
+ formulaPlaceholder = 'e.g. min(${pv} - ${gridExport}, ${consumption})';
17407
+ aggregationOptions = [
17408
+ { value: 'SUM', label: 'Sum' },
17409
+ { value: 'AVG', label: 'Average' },
17410
+ { value: 'MAX', label: 'Max' },
17411
+ { value: 'MIN', label: 'Min' },
17412
+ { value: 'COUNT', label: 'Count' },
17413
+ { value: 'TIME_WEIGHTED_AVG', label: 'Time-weighted average' }
17414
+ ];
17415
+ /** Rows are matched per category value (bar chart, AB#7953). */
17416
+ get categoryMode() {
17417
+ return this.alignment === 'category';
17418
+ }
17419
+ rows = [];
17420
+ errors = [];
17421
+ nextUid = 1;
17422
+ operandCache = new Map();
17423
+ /** The array last emitted, so an echo through `[series]` does not rebuild the rows. */
17424
+ lastEmitted = null;
17425
+ ngOnChanges(changes) {
17426
+ if (changes['series'] && this.series !== this.lastEmitted) {
17427
+ this.rows = (this.series ?? []).map(s => this.createRow(structuredClone(s)));
17428
+ this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
17429
+ for (const row of this.rows) {
17430
+ if (row.series.kind === 'query' && row.series.dataSource.queryRtId)
17431
+ void this.loadColumns(row);
17432
+ }
17433
+ }
17434
+ else if (changes['alignment']) {
17435
+ this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
17436
+ }
17437
+ }
17438
+ add() {
17439
+ this.rows = [...this.rows, this.createRow({
17440
+ kind: 'query',
17441
+ key: '',
17442
+ dataSource: { type: 'persistentQuery', queryRtId: '', queryFamily: 'streamData' },
17443
+ valueField: ''
17444
+ })];
17445
+ this.emit();
17446
+ this.focusAfterRender(this.rows.length - 1, 'key');
17447
+ }
17448
+ addFormula() {
17449
+ this.rows = [...this.rows, this.createRow({ kind: 'formula', key: '', formula: '' })];
17450
+ this.emit();
17451
+ this.focusAfterRender(this.rows.length - 1, 'key');
17452
+ }
17453
+ /** The row as query series (template narrowing). */
17454
+ asQuery(row) {
17455
+ return row.series.kind === 'query' ? row.series : null;
17456
+ }
17457
+ /** The row as formula series (template narrowing). */
17458
+ asFormula(row) {
17459
+ return row.series.kind === 'formula' ? row.series : null;
17460
+ }
17461
+ /**
17462
+ * Names a formula row can insert: the keys of every other row (a self-reference would be a
17463
+ * circular reference). Cached per key set, so the formula field's inputs keep their reference.
17464
+ */
17465
+ operandsFor(row) {
17466
+ const names = this.rows
17467
+ .filter(r => r !== row && SERIES_KEY_PATTERN.test(r.series.key?.trim() ?? ''))
17468
+ .map(r => r.series.key.trim());
17469
+ const cacheKey = names.join('\u0000');
17470
+ const cached = this.operandCache.get(row.uid);
17471
+ if (cached && cached.key === cacheKey)
17472
+ return cached.variables;
17473
+ const variables = names.map(name => ({ name, type: 'number', source: 'static', value: '' }));
17474
+ this.operandCache.set(row.uid, { key: cacheKey, variables });
17475
+ return variables;
17476
+ }
17477
+ /**
17478
+ * Message under a row. For a formula row the shared formula field already shows syntax and
17479
+ * unknown-key messages; the row repeats only what the field cannot know (key, circular reference).
17480
+ */
17481
+ rowMessage(index) {
17482
+ const message = this.errors[index] ?? null;
17483
+ const series = this.rows[index]?.series;
17484
+ if (!message || series?.kind !== 'formula' || !series.formula?.trim())
17485
+ return message;
17486
+ const keys = this.rows.map(r => r.series.key?.trim() ?? '').filter(k => !!k);
17487
+ const fieldError = validateFormula(series.formula, keys, this.evaluator).error;
17488
+ return message === fieldError ? null : message;
17489
+ }
17490
+ setOption(row, option, on) {
17491
+ const next = { ...row.series };
17492
+ if (on)
17493
+ delete next[option];
17494
+ else
17495
+ next[option] = false;
17496
+ row.series = next;
17497
+ this.emit();
17498
+ }
17499
+ setPrimaryOption(option, on) {
17500
+ const next = { ...(this.primary ?? {}) };
17501
+ if (on)
17502
+ delete next[option];
17503
+ else
17504
+ next[option] = false;
17505
+ this.primary = Object.keys(next).length > 0 ? next : undefined;
17506
+ this.primaryChange.emit(this.primary);
17507
+ this.cdr.markForCheck();
17508
+ }
17509
+ remove(index) {
17510
+ this.rows = this.rows.filter((_, i) => i !== index);
17511
+ this.emit();
17512
+ if (this.rows.length > 0)
17513
+ this.focusAfterRender(Math.min(index, this.rows.length - 1), 'down');
17514
+ else
17515
+ this.focusAdd();
17516
+ }
17517
+ move(index, delta) {
17518
+ const target = index + delta;
17519
+ if (target < 0 || target >= this.rows.length)
17520
+ return;
17521
+ const rows = [...this.rows];
17522
+ [rows[index], rows[target]] = [rows[target], rows[index]];
17523
+ this.rows = rows;
17524
+ this.emit();
17525
+ // Keep the focus on the moved row: on its button in the same direction, or the other one at an end.
17526
+ const atEnd = delta < 0 ? target === 0 : target === rows.length - 1;
17527
+ this.focusAfterRender(target, atEnd ? (delta < 0 ? 'down' : 'up') : (delta < 0 ? 'up' : 'down'));
17528
+ }
17529
+ async onQuerySelected(row, query) {
17530
+ if (row.series.kind !== 'query')
17531
+ return;
17532
+ row.query = query;
17533
+ row.series.dataSource = {
17534
+ ...row.series.dataSource,
17535
+ queryRtId: query?.rtId ?? '',
17536
+ queryName: query?.name || undefined,
17537
+ queryFamily: 'streamData'
17538
+ };
17539
+ row.series.valueField = '';
17540
+ if (this.categoryMode)
17541
+ row.series.categoryField = '';
17542
+ row.numericColumns = [];
17543
+ row.allColumns = [];
17544
+ this.emit();
17545
+ if (query) {
17546
+ await this.loadColumns(row);
17547
+ this.presetCategoryField(row);
17548
+ }
17549
+ }
17550
+ /**
17551
+ * Category mode: a row without a category field gets the primary query's category field when
17552
+ * its query has that column, else `timestamp` when it has one (AB#7953).
17553
+ */
17554
+ presetCategoryField(row) {
17555
+ if (!this.categoryMode || row.series.kind !== 'query' || row.series.categoryField)
17556
+ return;
17557
+ const paths = row.allColumns.map(c => c.attributePath);
17558
+ const preset = [this.defaultCategoryField, 'timestamp'].find(f => !!f && paths.includes(f));
17559
+ if (preset) {
17560
+ row.series.categoryField = preset;
17561
+ this.emit();
17562
+ }
17563
+ }
17564
+ validationOptions() {
17565
+ return { requireCategoryField: this.categoryMode };
17566
+ }
17567
+ onResolutionAwareChange(row, on) {
17568
+ if (row.series.kind !== 'query')
17569
+ return;
17570
+ row.series.dataSource = {
17571
+ ...row.series.dataSource,
17572
+ resolutionAware: on || undefined,
17573
+ requiredAggregation: on ? (row.series.dataSource.requiredAggregation ?? 'AVG') : undefined
17574
+ };
17575
+ this.emit();
17576
+ }
17577
+ emit() {
17578
+ const series = this.rows.map(r => ({ ...r.series, key: r.series.key?.trim() ?? '', label: r.series.label?.trim() || undefined }));
17579
+ this.errors = querySeriesErrors(series, this.evaluator, this.validationOptions());
17580
+ this.lastEmitted = series;
17581
+ this.seriesChange.emit(series);
17582
+ this.cdr.markForCheck();
17583
+ }
17584
+ createRow(series) {
17585
+ const ds = series.kind === 'query' ? series.dataSource : undefined;
17586
+ return {
17587
+ uid: this.nextUid++,
17588
+ series,
17589
+ query: ds?.queryRtId ? { rtId: ds.queryRtId, name: ds.queryName || ds.queryRtId } : null,
17590
+ numericColumns: [],
17591
+ allColumns: [],
17592
+ loadingColumns: false
17593
+ };
17594
+ }
17595
+ /** Numeric columns of the row's query, probed with the board window (AB#7975). */
17596
+ async loadColumns(row) {
17597
+ if (row.series.kind !== 'query')
17598
+ return;
17599
+ const queryRtId = row.series.dataSource.queryRtId;
17600
+ row.loadingColumns = true;
17601
+ this.cdr.markForCheck();
17602
+ try {
17603
+ const columns = await this.queryExecutor.fetchStreamDataColumns(queryRtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
17604
+ row.allColumns = columns
17605
+ .map(c => ({ attributePath: c.attributePath, attributeValueType: c.attributeValueType ?? '', aggregationType: c.aggregationType ?? null }));
17606
+ row.numericColumns = row.allColumns.filter(c => NUMERIC_TYPES.includes(c.attributeValueType));
17607
+ }
17608
+ catch (error) {
17609
+ console.warn('Could not load the columns of the series query:', error);
17610
+ row.numericColumns = [];
17611
+ row.allColumns = [];
17612
+ }
17613
+ finally {
17614
+ row.loadingColumns = false;
17615
+ this.cdr.markForCheck();
17616
+ }
17617
+ }
17618
+ focusAfterRender(index, target) {
17619
+ setTimeout(() => {
17620
+ const card = this.rowCards?.get(index)?.nativeElement;
17621
+ const selector = target === 'key' ? `[data-series-key="${index}"] input` : `[data-series-action="${target}"]`;
17622
+ const el = card?.querySelector(selector);
17623
+ (el && !el.disabled ? el : card)?.focus();
17624
+ });
17625
+ }
17626
+ focusAdd() {
17627
+ setTimeout(() => this.host.nativeElement.querySelector('[data-series-action="add"]')?.focus());
17628
+ }
17629
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17630
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartSeriesEditorComponent, isStandalone: true, selector: "mm-line-chart-series-editor", inputs: { series: "series", primary: "primary", stackedChart: "stackedChart", selectors: "selectors", alignment: "alignment", defaultCategoryField: "defaultCategoryField" }, outputs: { primaryChange: "primaryChange", seriesChange: "seriesChange" }, viewQueries: [{ propertyName: "rowCards", predicate: ["rowCard"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
17631
+ <div class="series-editor">
17632
+ <div class="series-row primary" role="group" aria-label="Series of the primary query">
17633
+ <span class="tag">primary</span>
17634
+ <span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
17635
+ <span class="display-options">
17636
+ <label class="checkbox">
17637
+ <input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
17638
+ (ngModelChange)="setPrimaryOption('visible', $event)" />
17639
+ Visible
17640
+ </label>
17641
+ @if (!categoryMode) {
17642
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17643
+ <input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
17644
+ [ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
17645
+ Stacked
17646
+ </label>
17647
+ }
17648
+ </span>
17649
+ </div>
17650
+
17651
+ @for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
17652
+ <div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
17653
+ <div class="row-actions">
17654
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
17655
+ data-series-action="up" [disabled]="first"
17656
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
17657
+ (click)="move(i, -1)"></button>
17658
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
17659
+ data-series-action="down" [disabled]="last"
17660
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
17661
+ (click)="move(i, 1)"></button>
17662
+ <span class="tag">{{ row.series.kind }}</span>
17663
+ </div>
17664
+
17665
+ <div class="field key">
17666
+ <label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
17667
+ <kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
17668
+ [(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
17669
+ </div>
17670
+ <div class="field label">
17671
+ <label [for]="'series-label-' + row.uid">Label</label>
17672
+ <kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
17673
+ (valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
17674
+ </div>
17675
+ @if (asQuery(row); as q) {
17676
+ <div class="field query">
17677
+ <label>Query (stream data) <span class="required">*</span></label>
17678
+ <mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
17679
+ (querySelected)="onQuerySelected(row, $event)"
17680
+ placeholder="Select a stream-data query..."></mm-query-selector>
17681
+ </div>
17682
+ @if (categoryMode) {
17683
+ <div class="field category">
17684
+ <label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
17685
+ <kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
17686
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17687
+ [loading]="row.loadingColumns" [allowCustom]="true"
17688
+ [(ngModel)]="q.categoryField" (valueChange)="emit()"
17689
+ placeholder="e.g. timestamp"></kendo-combobox>
17690
+ </div>
17691
+ }
17692
+ <div class="field value">
17693
+ <label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
17694
+ <kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
17695
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17696
+ [loading]="row.loadingColumns" [allowCustom]="true"
17697
+ [(ngModel)]="q.valueField" (valueChange)="emit()"
17698
+ placeholder="Numeric column..."></kendo-combobox>
17699
+ </div>
17700
+ <div class="field scope">
17701
+ <mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
17702
+ [entitySelectorId]="q.dataSource.entitySelectorId"
17703
+ (entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
17704
+ </mm-entity-selector-scope-picker>
17705
+ </div>
17706
+ <div class="field toggles">
17707
+ <mm-sd-time-filter-toggle [family]="'streamData'"
17708
+ [ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
17709
+ (ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
17710
+ </mm-sd-time-filter-toggle>
17711
+ @if (!categoryMode) {
17712
+ <label class="checkbox">
17713
+ <input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
17714
+ (ngModelChange)="onResolutionAwareChange(row, $event)" />
17715
+ Resolution-aware
17716
+ </label>
17717
+ }
17718
+ @if (!categoryMode && q.dataSource.resolutionAware) {
17719
+ <kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
17720
+ [valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
17721
+ [(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
17722
+ </kendo-dropdownlist>
17723
+ }
17724
+ </div>
17725
+ } @else if (asFormula(row); as f) {
17726
+ <div class="field formula">
17727
+ <mm-meshboard-formula-field
17728
+ [formula]="f.formula"
17729
+ (formulaChange)="f.formula = $event; emit()"
17730
+ [variables]="operandsFor(row)"
17731
+ [showPreview]="false"
17732
+ label="Formula"
17733
+ variablesLabel="Available series"
17734
+ namesNoun="series"
17735
+ [syntaxHint]="keySyntaxHint"
17736
+ emptyVariablesText="No other series yet. Add query series first."
17737
+ [placeholder]="formulaPlaceholder">
17738
+ </mm-meshboard-formula-field>
17739
+ </div>
17740
+ }
17741
+ <div class="field display-options">
17742
+ <label class="checkbox">
17743
+ <input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
17744
+ (ngModelChange)="setOption(row, 'visible', $event)" />
17745
+ Visible
17746
+ </label>
17747
+ @if (!categoryMode) {
17748
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17749
+ <input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
17750
+ [ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
17751
+ Stacked
17752
+ </label>
17753
+ }
17754
+ </div>
17755
+ <button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
17756
+ data-series-action="remove"
17757
+ [attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
17758
+ (click)="remove(i)"></button>
17759
+ @if (rowMessage(i); as message) {
17760
+ <p class="row-error" role="alert">{{ message }}</p>
17761
+ }
17762
+ </div>
17763
+ }
17764
+
17765
+ <div class="add-actions">
17766
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
17767
+ Add query series
17768
+ </button>
17769
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
17770
+ Add formula series
17771
+ </button>
17772
+ </div>
17773
+ </div>
17774
+ `, isInline: true, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
17775
+ }
17776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, decorators: [{
17777
+ type: Component,
17778
+ args: [{ selector: 'mm-line-chart-series-editor', standalone: true, imports: [
17779
+ FormsModule,
17780
+ ButtonModule,
17781
+ DropDownsModule,
17782
+ InputsModule,
17783
+ QuerySelectorComponent,
17784
+ SdTimeFilterToggleComponent,
17785
+ EntitySelectorScopePickerComponent,
17786
+ FormulaFieldComponent
17787
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
17788
+ <div class="series-editor">
17789
+ <div class="series-row primary" role="group" aria-label="Series of the primary query">
17790
+ <span class="tag">primary</span>
17791
+ <span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
17792
+ <span class="display-options">
17793
+ <label class="checkbox">
17794
+ <input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
17795
+ (ngModelChange)="setPrimaryOption('visible', $event)" />
17796
+ Visible
17797
+ </label>
17798
+ @if (!categoryMode) {
17799
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17800
+ <input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
17801
+ [ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
17802
+ Stacked
17803
+ </label>
17804
+ }
17805
+ </span>
17806
+ </div>
17807
+
17808
+ @for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
17809
+ <div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
17810
+ <div class="row-actions">
17811
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
17812
+ data-series-action="up" [disabled]="first"
17813
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
17814
+ (click)="move(i, -1)"></button>
17815
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
17816
+ data-series-action="down" [disabled]="last"
17817
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
17818
+ (click)="move(i, 1)"></button>
17819
+ <span class="tag">{{ row.series.kind }}</span>
17820
+ </div>
17821
+
17822
+ <div class="field key">
17823
+ <label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
17824
+ <kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
17825
+ [(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
17826
+ </div>
17827
+ <div class="field label">
17828
+ <label [for]="'series-label-' + row.uid">Label</label>
17829
+ <kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
17830
+ (valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
17831
+ </div>
17832
+ @if (asQuery(row); as q) {
17833
+ <div class="field query">
17834
+ <label>Query (stream data) <span class="required">*</span></label>
17835
+ <mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
17836
+ (querySelected)="onQuerySelected(row, $event)"
17837
+ placeholder="Select a stream-data query..."></mm-query-selector>
17838
+ </div>
17839
+ @if (categoryMode) {
17840
+ <div class="field category">
17841
+ <label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
17842
+ <kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
17843
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17844
+ [loading]="row.loadingColumns" [allowCustom]="true"
17845
+ [(ngModel)]="q.categoryField" (valueChange)="emit()"
17846
+ placeholder="e.g. timestamp"></kendo-combobox>
17847
+ </div>
17848
+ }
17849
+ <div class="field value">
17850
+ <label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
17851
+ <kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
17852
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17853
+ [loading]="row.loadingColumns" [allowCustom]="true"
17854
+ [(ngModel)]="q.valueField" (valueChange)="emit()"
17855
+ placeholder="Numeric column..."></kendo-combobox>
17856
+ </div>
17857
+ <div class="field scope">
17858
+ <mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
17859
+ [entitySelectorId]="q.dataSource.entitySelectorId"
17860
+ (entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
17861
+ </mm-entity-selector-scope-picker>
17862
+ </div>
17863
+ <div class="field toggles">
17864
+ <mm-sd-time-filter-toggle [family]="'streamData'"
17865
+ [ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
17866
+ (ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
17867
+ </mm-sd-time-filter-toggle>
17868
+ @if (!categoryMode) {
17869
+ <label class="checkbox">
17870
+ <input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
17871
+ (ngModelChange)="onResolutionAwareChange(row, $event)" />
17872
+ Resolution-aware
17873
+ </label>
17874
+ }
17875
+ @if (!categoryMode && q.dataSource.resolutionAware) {
17876
+ <kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
17877
+ [valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
17878
+ [(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
17879
+ </kendo-dropdownlist>
17880
+ }
17881
+ </div>
17882
+ } @else if (asFormula(row); as f) {
17883
+ <div class="field formula">
17884
+ <mm-meshboard-formula-field
17885
+ [formula]="f.formula"
17886
+ (formulaChange)="f.formula = $event; emit()"
17887
+ [variables]="operandsFor(row)"
17888
+ [showPreview]="false"
17889
+ label="Formula"
17890
+ variablesLabel="Available series"
17891
+ namesNoun="series"
17892
+ [syntaxHint]="keySyntaxHint"
17893
+ emptyVariablesText="No other series yet. Add query series first."
17894
+ [placeholder]="formulaPlaceholder">
17895
+ </mm-meshboard-formula-field>
17896
+ </div>
17897
+ }
17898
+ <div class="field display-options">
17899
+ <label class="checkbox">
17900
+ <input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
17901
+ (ngModelChange)="setOption(row, 'visible', $event)" />
17902
+ Visible
17903
+ </label>
17904
+ @if (!categoryMode) {
17905
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17906
+ <input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
17907
+ [ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
17908
+ Stacked
17909
+ </label>
17910
+ }
17911
+ </div>
17912
+ <button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
17913
+ data-series-action="remove"
17914
+ [attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
17915
+ (click)="remove(i)"></button>
17916
+ @if (rowMessage(i); as message) {
17917
+ <p class="row-error" role="alert">{{ message }}</p>
17918
+ }
17919
+ </div>
17920
+ }
17921
+
17922
+ <div class="add-actions">
17923
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
17924
+ Add query series
17925
+ </button>
17926
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
17927
+ Add formula series
17928
+ </button>
17929
+ </div>
17930
+ </div>
17931
+ `, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"] }]
17932
+ }], propDecorators: { series: [{
17933
+ type: Input
17934
+ }], primary: [{
17935
+ type: Input
17936
+ }], stackedChart: [{
17937
+ type: Input
17938
+ }], primaryChange: [{
17939
+ type: Output
17940
+ }], selectors: [{
17941
+ type: Input
17942
+ }], alignment: [{
17943
+ type: Input
17944
+ }], defaultCategoryField: [{
17945
+ type: Input
17946
+ }], seriesChange: [{
17947
+ type: Output
17948
+ }], rowCards: [{
17949
+ type: ViewChildren,
17950
+ args: ['rowCard']
17951
+ }] } });
17952
+
16520
17953
  /**
16521
17954
  * Configuration dialog for Bar Chart widgets.
16522
17955
  * Supports column, bar, stacked, and 100% stacked chart types.
@@ -16526,6 +17959,7 @@ class BarChartConfigDialogComponent {
16526
17959
  queryExecutor = inject(QueryExecutorService);
16527
17960
  stateService = inject(MeshBoardStateService);
16528
17961
  windowRef = inject(WindowRef);
17962
+ expressionEvaluator = inject(ExpressionEvaluatorService);
16529
17963
  querySelector;
16530
17964
  // Initial values for editing
16531
17965
  initialQueryRtId;
@@ -16544,6 +17978,16 @@ class BarChartConfigDialogComponent {
16544
17978
  initialColorThresholds;
16545
17979
  initialDefaultBarColor;
16546
17980
  initialFilters;
17981
+ initialAdditionalSeries;
17982
+ initialPrimarySeries;
17983
+ /** Query and formula series of the Series section (AB#7953). */
17984
+ additionalSeries = [];
17985
+ /** Display options of the primary query's series (AB#7953). */
17986
+ primarySeries;
17987
+ /** The series that are saved: none unless the primary query is a stream-data query. */
17988
+ get effectiveAdditionalSeries() {
17989
+ return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
17990
+ }
16547
17991
  // State
16548
17992
  isLoadingInitial = false;
16549
17993
  isLoadingColumns = false;
@@ -16599,6 +18043,10 @@ class BarChartConfigDialogComponent {
16599
18043
  if (this.selectedPersistentQuery === null || this.form.categoryField === '') {
16600
18044
  return false;
16601
18045
  }
18046
+ const seriesValid = querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator, { requireCategoryField: true })
18047
+ .every(e => e === null);
18048
+ if (!seriesValid)
18049
+ return false;
16602
18050
  if (this.seriesMode === 'dynamic') {
16603
18051
  return this.seriesGroupField !== '' && this.valueField !== '';
16604
18052
  }
@@ -16623,6 +18071,8 @@ class BarChartConfigDialogComponent {
16623
18071
  this.form.showDataLabels = this.initialShowDataLabels ?? false;
16624
18072
  this.form.colorThresholds = this.initialColorThresholds ? [...this.initialColorThresholds] : [];
16625
18073
  this.form.defaultBarColor = this.initialDefaultBarColor ?? '';
18074
+ this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
18075
+ this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
16626
18076
  // Determine series mode from initial values
16627
18077
  if (this.initialSeriesGroupField && this.initialValueField) {
16628
18078
  this.seriesMode = 'dynamic';
@@ -16730,8 +18180,10 @@ class BarChartConfigDialogComponent {
16730
18180
  aggregationType: c.aggregationType ?? null
16731
18181
  }));
16732
18182
  }
16733
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
16734
- return sdResult.columns.map(c => ({
18183
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
18184
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
18185
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
18186
+ return columns.map(c => ({
16735
18187
  attributePath: c.attributePath,
16736
18188
  attributeValueType: c.attributeValueType ?? '',
16737
18189
  aggregationType: c.aggregationType ?? null
@@ -16783,7 +18235,9 @@ class BarChartConfigDialogComponent {
16783
18235
  showDataLabels: this.form.showDataLabels,
16784
18236
  colorThresholds: this.form.colorThresholds.length > 0 ? this.form.colorThresholds : undefined,
16785
18237
  defaultBarColor: this.form.defaultBarColor || undefined,
16786
- filters: filtersDto
18238
+ filters: filtersDto,
18239
+ additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
18240
+ primarySeries: this.selectedQueryFamily === 'streamData' && this.primarySeries?.visible === false ? { visible: false } : undefined
16787
18241
  };
16788
18242
  // Add dynamic series fields if in dynamic mode
16789
18243
  if (this.seriesMode === 'dynamic') {
@@ -16802,7 +18256,7 @@ class BarChartConfigDialogComponent {
16802
18256
  this.windowRef.close();
16803
18257
  }
16804
18258
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16805
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
18259
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
16806
18260
  <div class="config-container">
16807
18261
 
16808
18262
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -16941,6 +18395,23 @@ class BarChartConfigDialogComponent {
16941
18395
  </div>
16942
18396
  }
16943
18397
 
18398
+ <!-- Series Section (AB#7953): query and formula series matched per category -->
18399
+ @if (selectedQueryFamily === 'streamData') {
18400
+ <div class="config-section">
18401
+ <h3 class="section-title">Series</h3>
18402
+ <p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
18403
+ <mm-line-chart-series-editor
18404
+ alignment="category"
18405
+ [series]="additionalSeries"
18406
+ [selectors]="availableEntitySelectors"
18407
+ [primary]="primarySeries"
18408
+ [defaultCategoryField]="form.categoryField"
18409
+ (seriesChange)="additionalSeries = $event"
18410
+ (primaryChange)="primarySeries = $event">
18411
+ </mm-line-chart-series-editor>
18412
+ </div>
18413
+ }
18414
+
16944
18415
  <!-- Filters Section -->
16945
18416
  @if (selectedPersistentQuery?.queryCkTypeId) {
16946
18417
  <div class="config-section">
@@ -17074,7 +18545,7 @@ class BarChartConfigDialogComponent {
17074
18545
  </button>
17075
18546
  </div>
17076
18547
  </div>
17077
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18548
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17078
18549
  }
17079
18550
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
17080
18551
  type: Component,
@@ -17089,7 +18560,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17089
18560
  QuerySelectorComponent,
17090
18561
  SdTimeFilterToggleComponent,
17091
18562
  EntitySelectorScopePickerComponent,
17092
- LoadingOverlayComponent
18563
+ LoadingOverlayComponent,
18564
+ LineChartSeriesEditorComponent
17093
18565
  ], template: `
17094
18566
  <div class="config-container">
17095
18567
 
@@ -17229,6 +18701,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17229
18701
  </div>
17230
18702
  }
17231
18703
 
18704
+ <!-- Series Section (AB#7953): query and formula series matched per category -->
18705
+ @if (selectedQueryFamily === 'streamData') {
18706
+ <div class="config-section">
18707
+ <h3 class="section-title">Series</h3>
18708
+ <p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
18709
+ <mm-line-chart-series-editor
18710
+ alignment="category"
18711
+ [series]="additionalSeries"
18712
+ [selectors]="availableEntitySelectors"
18713
+ [primary]="primarySeries"
18714
+ [defaultCategoryField]="form.categoryField"
18715
+ (seriesChange)="additionalSeries = $event"
18716
+ (primaryChange)="primarySeries = $event">
18717
+ </mm-line-chart-series-editor>
18718
+ </div>
18719
+ }
18720
+
17232
18721
  <!-- Filters Section -->
17233
18722
  @if (selectedPersistentQuery?.queryCkTypeId) {
17234
18723
  <div class="config-section">
@@ -17398,8 +18887,179 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17398
18887
  type: Input
17399
18888
  }], initialFilters: [{
17400
18889
  type: Input
18890
+ }], initialAdditionalSeries: [{
18891
+ type: Input
18892
+ }], initialPrimarySeries: [{
18893
+ type: Input
17401
18894
  }] } });
17402
18895
 
18896
+ /**
18897
+ * Aligns the series of several stream-data sources on one time grid (AB#7951).
18898
+ *
18899
+ * Every source delivers its points keyed by the bucket start instant (epoch ms). Each source's
18900
+ * bucket step is its most frequent distance between consecutive instants (gaps in the data do not
18901
+ * change it). The grid step is the COARSEST source step; the source that has it (the first one on a
18902
+ * tie, i.e. the primary source when it is passed first) is the reference and gives the anchor.
18903
+ *
18904
+ * - Fixed-size grid (step up to one hour): a source is aligned when every instant is a whole number
18905
+ * of grid steps away from the reference anchor. A finer rung (5-min buckets on a 15-min grid) or
18906
+ * a shifted anchor puts instants between grid points: that source is reported as misaligned and
18907
+ * left out instead of being plotted shifted. Missing grid instants between the first and the last
18908
+ * instant are filled in, so a bucket no source has data for is still a gap.
18909
+ * - Calendar grid (longer steps: days, months, whose real length changes with DST and month
18910
+ * length): a source is aligned when none of its instants lies strictly inside a reference bucket
18911
+ * (0 < distance to the nearest reference instant < step). Instants only, no wall-clock
18912
+ * arithmetic, so days across a DST change (23 h / 25 h) align when the sources share them. No
18913
+ * fill-in.
18914
+ *
18915
+ * Gaps: a grid instant a series has no point for is `null` (rendered as a gap, never 0).
18916
+ */
18917
+ /** Largest fixed-size grid step: modular alignment check and fill-in up to here, calendar rule above. */
18918
+ const MAX_FILLED_GRID_STEP_MS = 60 * 60 * 1000;
18919
+ /** Instants a source needs for its step to count when the reference is chosen. */
18920
+ const MIN_REFERENCE_INSTANTS = 3;
18921
+ /** Upper bound of filled grid instants, guarding against a degenerate step over a long window. */
18922
+ const MAX_FILLED_GRID_POINTS = 20000;
18923
+ /** Sorted distinct instants of all series of a source. */
18924
+ function sourceInstants(source) {
18925
+ const set = new Set();
18926
+ for (const s of source.series) {
18927
+ for (const t of s.points.keys()) {
18928
+ if (Number.isFinite(t))
18929
+ set.add(t);
18930
+ }
18931
+ }
18932
+ return Array.from(set).sort((a, b) => a - b);
18933
+ }
18934
+ /**
18935
+ * Typical bucket step of sorted instants: the most frequent positive distance between consecutive
18936
+ * instants (the smaller one on a tie), `null` for fewer than two instants.
18937
+ */
18938
+ function bucketStepMs(instants) {
18939
+ const counts = new Map();
18940
+ for (let i = 1; i < instants.length; i++) {
18941
+ const d = instants[i] - instants[i - 1];
18942
+ if (d > 0)
18943
+ counts.set(d, (counts.get(d) ?? 0) + 1);
18944
+ }
18945
+ let step = null;
18946
+ let best = 0;
18947
+ for (const [d, n] of counts) {
18948
+ if (n > best || (n === best && step !== null && d < step)) {
18949
+ step = d;
18950
+ best = n;
18951
+ }
18952
+ }
18953
+ return step;
18954
+ }
18955
+ /** Distance from `t` to the nearest value in the sorted array `sorted` (Infinity when empty). */
18956
+ function distanceToNearest(sorted, t) {
18957
+ let lo = 0;
18958
+ let hi = sorted.length - 1;
18959
+ if (hi < 0)
18960
+ return Infinity;
18961
+ while (lo < hi) {
18962
+ const mid = (lo + hi) >> 1;
18963
+ if (sorted[mid] < t)
18964
+ lo = mid + 1;
18965
+ else
18966
+ hi = mid;
18967
+ }
18968
+ let best = Math.abs(sorted[lo] - t);
18969
+ if (lo > 0)
18970
+ best = Math.min(best, Math.abs(sorted[lo - 1] - t));
18971
+ return best;
18972
+ }
18973
+ /**
18974
+ * Aligns the sources on one grid. Source order is kept for the series; the first source wins a
18975
+ * tie for the coarsest step (the primary source when it is passed first).
18976
+ */
18977
+ function alignOnTimeGrid(sources) {
18978
+ const perSource = sources.map(source => {
18979
+ const instants = sourceInstants(source);
18980
+ return { source, instants, step: bucketStepMs(instants) };
18981
+ });
18982
+ // Reference = coarsest step; the first source wins a tie.
18983
+ // Reference = coarsest step; the first source wins a tie. A source with fewer than
18984
+ // MIN_REFERENCE_INSTANTS instants has no reliable step (two sparse points look like a coarse
18985
+ // rung) and is the reference only when no source has enough instants.
18986
+ const eligible = perSource.some(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
18987
+ ? perSource.filter(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
18988
+ : perSource;
18989
+ let reference = null;
18990
+ for (const entry of eligible) {
18991
+ if (entry.step !== null && (reference === null || entry.step > (reference.step ?? 0))) {
18992
+ reference = entry;
18993
+ }
18994
+ }
18995
+ const gridStepMs = reference?.step ?? null;
18996
+ const misaligned = [];
18997
+ const aligned = [];
18998
+ for (const entry of perSource) {
18999
+ if (reference && gridStepMs !== null && entry !== reference) {
19000
+ const anchor = reference.instants[0];
19001
+ const off = gridStepMs <= MAX_FILLED_GRID_STEP_MS
19002
+ ? entry.instants.some(t => (t - anchor) % gridStepMs !== 0)
19003
+ : entry.instants.some(t => {
19004
+ const d = distanceToNearest(reference.instants, t);
19005
+ return d > 0 && d < gridStepMs;
19006
+ });
19007
+ if (off) {
19008
+ misaligned.push({
19009
+ sourceKey: entry.source.sourceKey,
19010
+ sourceLabel: entry.source.sourceLabel,
19011
+ stepMs: entry.step,
19012
+ gridStepMs
19013
+ });
19014
+ continue;
19015
+ }
19016
+ }
19017
+ aligned.push(entry);
19018
+ }
19019
+ const instantSet = new Set();
19020
+ for (const entry of aligned) {
19021
+ for (const t of entry.instants)
19022
+ instantSet.add(t);
19023
+ }
19024
+ let instants = Array.from(instantSet).sort((a, b) => a - b);
19025
+ if (gridStepMs !== null && gridStepMs <= MAX_FILLED_GRID_STEP_MS && instants.length > 1) {
19026
+ instants = fillGrid(instants, gridStepMs);
19027
+ }
19028
+ const series = [];
19029
+ for (const entry of aligned) {
19030
+ for (const s of entry.source.series) {
19031
+ series.push({
19032
+ name: s.name,
19033
+ sourceKey: entry.source.sourceKey,
19034
+ data: instants.map(t => s.points.get(t) ?? null),
19035
+ unit: s.unit
19036
+ });
19037
+ }
19038
+ }
19039
+ return { instants, gridStepMs, series, misaligned };
19040
+ }
19041
+ /**
19042
+ * Adds the missing multiples of `step` between consecutive instants whose distance is a whole
19043
+ * multiple of it. Distances that are not a multiple (a source-own instant inside a reference gap)
19044
+ * are left as they are.
19045
+ */
19046
+ function fillGrid(instants, step) {
19047
+ const span = instants[instants.length - 1] - instants[0];
19048
+ if (span / step > MAX_FILLED_GRID_POINTS)
19049
+ return instants;
19050
+ const out = [instants[0]];
19051
+ for (let i = 1; i < instants.length; i++) {
19052
+ const prev = instants[i - 1];
19053
+ const d = instants[i] - prev;
19054
+ if (d > step && d % step === 0) {
19055
+ for (let t = prev + step; t < instants[i]; t += step)
19056
+ out.push(t);
19057
+ }
19058
+ out.push(instants[i]);
19059
+ }
19060
+ return out;
19061
+ }
19062
+
17403
19063
  /** Series colours so a series' min/max band and its avg line share one hue. */
17404
19064
  const SERIES_PALETTE = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
17405
19065
  /**
@@ -17429,6 +19089,7 @@ class LineChartWidgetComponent {
17429
19089
  ]);
17430
19090
  stateService = inject(MeshBoardStateService);
17431
19091
  variableService = inject(MeshBoardVariableService);
19092
+ expressionEvaluator = inject(ExpressionEvaluatorService);
17432
19093
  config;
17433
19094
  // Widget state signals
17434
19095
  _isLoading = signal(false, /* @ts-ignore */
@@ -17466,6 +19127,10 @@ class LineChartWidgetComponent {
17466
19127
  valueAxes = this._valueAxes.asReadonly();
17467
19128
  error = this._error.asReadonly();
17468
19129
  dataInfo = this._dataInfo.asReadonly();
19130
+ // Per-source hints of a multi-source chart (AB#7951); always empty for a single-query chart.
19131
+ _sourceHints = signal([], /* @ts-ignore */
19132
+ ...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
19133
+ sourceHints = this._sourceHints.asReadonly();
17469
19134
  /**
17470
19135
  * Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
17471
19136
  * `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
@@ -17608,6 +19273,49 @@ class LineChartWidgetComponent {
17608
19273
  return this.config?.chartType ?? 'line';
17609
19274
  }, /* @ts-ignore */
17610
19275
  ...(ngDevMode ? [{ debugName: "chartType" }] : /* istanbul ignore next */ []));
19276
+ /**
19277
+ * Stacking (AB#7951): only an area chart with `stacked` stacks; the series stack in their order
19278
+ * (primary series first, then `additionalSeries`). `false` is Kendo's default, so an existing
19279
+ * chart stacks exactly as before (not at all).
19280
+ */
19281
+ stackSeries() {
19282
+ return !!this.config?.stacked && this.chartType() === 'area';
19283
+ }
19284
+ /**
19285
+ * Kendo `missingValues` (AB#7951), always set explicitly. A stacked or multi-source chart uses
19286
+ * `'gap'`: a bucket without data is a gap, never drawn or stacked as 0 and never bridged.
19287
+ * Single-query unstacked charts get exactly Kendo's defaults — `'interpolate'` for a line,
19288
+ * `'zero'` for an area series — so they render as before.
19289
+ */
19290
+ missingValues() {
19291
+ if (this.stackSeries() || this.hasAdditionalSeries())
19292
+ return 'gap';
19293
+ return this.chartType() === 'area' ? 'zero' : 'interpolate';
19294
+ }
19295
+ /** Whether the chart reads series from more than its primary query (AB#7951) or has formula series (AB#7952). */
19296
+ hasAdditionalSeries() {
19297
+ return (this.config?.additionalSeries ?? []).some(s => isCompleteQuerySeries(s) || isCompleteFormulaSeries(s));
19298
+ }
19299
+ /** Whether the last load went through the multi-source path (aligned grid, AB#7951/AB#7952). */
19300
+ _multiSource = signal(false, /* @ts-ignore */
19301
+ ...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
19302
+ isMultiSource = this._multiSource.asReadonly();
19303
+ /**
19304
+ * Shared tooltip rows of the multi-source path (AB#7952): one row per drawn series for the
19305
+ * hovered category; a gap (null) shows "–".
19306
+ */
19307
+ tooltipRows(category) {
19308
+ const index = this._categories().indexOf(category);
19309
+ if (index < 0)
19310
+ return [];
19311
+ return this._seriesData().map(s => {
19312
+ const value = s.data[index];
19313
+ return {
19314
+ name: s.name,
19315
+ text: value === null || value === undefined ? '–' : `${this.formatValue(value)}${this.getUnitForSeries(s.name)}`
19316
+ };
19317
+ });
19318
+ }
17611
19319
  labelRotation = computed(() => {
17612
19320
  const categoryCount = this._categories().length;
17613
19321
  return categoryCount > 5 ? -45 : 0;
@@ -17709,6 +19417,12 @@ class LineChartWidgetComponent {
17709
19417
  async loadData() {
17710
19418
  if (this.isNotConfigured())
17711
19419
  return;
19420
+ this._multiSource.set(this.hasAdditionalSeries());
19421
+ if (this.hasAdditionalSeries()) {
19422
+ await this.loadMultiSource();
19423
+ return;
19424
+ }
19425
+ this._sourceHints.set([]);
17712
19426
  const queryDataSource = this.config.dataSource;
17713
19427
  this._isLoading.set(true);
17714
19428
  this._error.set(null);
@@ -17890,6 +19604,240 @@ class LineChartWidgetComponent {
17890
19604
  ]
17891
19605
  };
17892
19606
  }
19607
+ /**
19608
+ * Multi-source load (AB#7951): the primary data source plus every complete query series, each
19609
+ * with its own scope, time-filter opt-out and resolution-aware routing, aligned on one time grid.
19610
+ *
19611
+ * 1. Scope: any source pending → stay loading, no query; an empty scope → the source has no
19612
+ * series and a "No data for <source>" hint.
19613
+ * 2. Prepare: resolution-aware sources resolve their rollup rung.
19614
+ * 3. Common bucket count: the smallest of the pixel-derived target and every resolved
19615
+ * `points` (= the coarsest effective bucket). Every source is downsampled with this count over
19616
+ * the same window, so all of them use the same bucket width.
19617
+ * 4. Fetch all sources in parallel; one failing source gives an error hint, the others render.
19618
+ * 5. {@link alignOnTimeGrid}: shared categories, gaps as null, misaligned sources left out with a hint.
19619
+ */
19620
+ async loadMultiSource() {
19621
+ const primaryDs = this.config.dataSource;
19622
+ const sources = [
19623
+ {
19624
+ key: PRIMARY_SOURCE_KEY,
19625
+ label: primaryDs.queryName?.trim() || 'Primary query',
19626
+ dataSource: primaryDs,
19627
+ valueField: this.config.valueField,
19628
+ primary: true
19629
+ },
19630
+ ...(this.config.additionalSeries ?? [])
19631
+ .filter(isCompleteQuerySeries)
19632
+ .map((def) => ({
19633
+ key: def.key,
19634
+ label: querySeriesLabel(def),
19635
+ dataSource: def.dataSource,
19636
+ valueField: def.valueField,
19637
+ primary: false
19638
+ }))
19639
+ ];
19640
+ this._isLoading.set(true);
19641
+ this._error.set(null);
19642
+ const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
19643
+ if (scopes.some(s => s.kind === 'pending')) {
19644
+ return;
19645
+ }
19646
+ try {
19647
+ const hints = [];
19648
+ const problems = new Map();
19649
+ const active = [];
19650
+ sources.forEach((source, i) => {
19651
+ if (scopes[i].kind === 'empty') {
19652
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
19653
+ problems.set(source.key, { label: source.label, problem: 'noData' });
19654
+ }
19655
+ else {
19656
+ active.push(source);
19657
+ }
19658
+ });
19659
+ const prepared = await Promise.allSettled(active.map(s => this.prepareSource(s)));
19660
+ let limit = this.computeDownsampleLimit();
19661
+ const targetPoints = limit;
19662
+ let signal = null;
19663
+ for (const p of prepared) {
19664
+ const resolution = p.status === 'fulfilled' ? p.value.resolutionAware?.resolution : null;
19665
+ if (resolution) {
19666
+ limit = Math.min(limit, Math.max(1, resolution.points));
19667
+ signal ??= resolution;
19668
+ }
19669
+ }
19670
+ const fetched = await Promise.allSettled(active.map((source, i) => {
19671
+ const p = prepared[i];
19672
+ return p.status === 'fulfilled'
19673
+ ? this.fetchSource(source, p.value, limit)
19674
+ : Promise.reject(p.reason);
19675
+ }));
19676
+ const gridSources = [];
19677
+ let rowCount = 0;
19678
+ fetched.forEach((f, i) => {
19679
+ const source = active[i];
19680
+ if (f.status === 'rejected') {
19681
+ console.error(`Error loading Line Chart source "${source.label}":`, f.reason);
19682
+ hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
19683
+ problems.set(source.key, { label: source.label, problem: 'error' });
19684
+ return;
19685
+ }
19686
+ rowCount += f.value.rows.length;
19687
+ const series = source.primary
19688
+ ? primaryRowsToSeries(f.value.rows, this.config, f.value.downsampled)
19689
+ : [queryRowsToSeries(f.value.rows, source.label, source.valueField, f.value.downsampled)];
19690
+ const withData = series.filter(s => s.points.size > 0);
19691
+ if (withData.length === 0) {
19692
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
19693
+ problems.set(source.key, { label: source.label, problem: 'noData' });
19694
+ return;
19695
+ }
19696
+ gridSources.push({ sourceKey: source.key, sourceLabel: source.label, series: withData });
19697
+ });
19698
+ const grid = alignOnTimeGrid(gridSources);
19699
+ for (const m of grid.misaligned) {
19700
+ hints.push({
19701
+ kind: 'misaligned',
19702
+ sourceLabel: m.sourceLabel,
19703
+ text: `${m.sourceLabel}: its buckets do not match the chart's time grid and are not shown`
19704
+ });
19705
+ problems.set(m.sourceKey, { label: m.sourceLabel, problem: 'misaligned' });
19706
+ }
19707
+ // Formula series, per-series visibility and stacking (AB#7952).
19708
+ const composed = composeChartSeries({
19709
+ gridSeries: grid.series,
19710
+ length: grid.instants.length,
19711
+ definitions: this.config.additionalSeries ?? [],
19712
+ primary: this.config.primarySeries,
19713
+ stackedChart: this.stackSeries(),
19714
+ sourceProblems: problems,
19715
+ evaluator: this.expressionEvaluator
19716
+ });
19717
+ hints.push(...composed.hints);
19718
+ this.applyAlignedGrid(grid.instants, composed.series);
19719
+ this._sourceHints.set(hints);
19720
+ this._resolutionSignal.set(signal);
19721
+ this._resolutionTarget.set(signal ? targetPoints : null);
19722
+ this.lastLimit = limit;
19723
+ this._dataInfo.set({ rows: rowCount, points: grid.instants.length, total: rowCount });
19724
+ this._isLoading.set(false);
19725
+ }
19726
+ catch (err) {
19727
+ console.error('Error loading Line Chart data:', err);
19728
+ this._error.set('Failed to load data');
19729
+ this._dataInfo.set(null);
19730
+ this._resolutionSignal.set(null);
19731
+ this._sourceHints.set([]);
19732
+ this._isLoading.set(false);
19733
+ }
19734
+ }
19735
+ /** Resolution-aware sources resolve their rung first (step 2 of {@link loadMultiSource}). */
19736
+ async prepareSource(source) {
19737
+ const ds = source.dataSource;
19738
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
19739
+ const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
19740
+ if (!(ds.resolutionAware && ds.queryFamily === 'streamData' && source.valueField && timeArgs?.from && timeArgs?.to)) {
19741
+ return { timeArgs, rtIds };
19742
+ }
19743
+ const info = await this.queryExecutor.fetchQueryArchive(ds.queryRtId);
19744
+ if (!info) {
19745
+ throw new Error('base archive not found for the selected query');
19746
+ }
19747
+ const scopeRtIds = rtIds && rtIds.length > 0 ? rtIds : (info.rtIds ?? undefined);
19748
+ const resolution = await this.queryExecutor.resolveSeriesQuery({
19749
+ baseArchiveRtId: info.archiveRtId,
19750
+ from: timeArgs.from,
19751
+ to: timeArgs.to,
19752
+ targetPoints: this.computeDownsampleLimit(),
19753
+ requiredAggregation: (ds.requiredAggregation ?? 'SUM'),
19754
+ sourcePath: source.valueField,
19755
+ rtIds: scopeRtIds,
19756
+ timeZone: this.stateService.resolveStreamDataTimeZone()
19757
+ });
19758
+ return {
19759
+ timeArgs,
19760
+ rtIds,
19761
+ resolutionAware: { resolution, ckTypeId: info.ckTypeId, scopeRtIds, from: timeArgs.from, to: timeArgs.to }
19762
+ };
19763
+ }
19764
+ /** Fetches one source with the common bucket count (step 4 of {@link loadMultiSource}). */
19765
+ async fetchSource(source, prepared, limit) {
19766
+ const ds = source.dataSource;
19767
+ // Widget filters belong to the primary query's type; query series run unfiltered.
19768
+ const fieldFilter = source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined;
19769
+ const ra = prepared.resolutionAware;
19770
+ if (ra) {
19771
+ const resolution = ra.resolution;
19772
+ if (!resolution)
19773
+ return { rows: [], downsampled: true };
19774
+ const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
19775
+ archiveRtId: resolution.archiveRtId,
19776
+ from: ra.from,
19777
+ to: ra.to,
19778
+ limit,
19779
+ sourcePath: source.valueField,
19780
+ aggregation: resolution.reducingFunction,
19781
+ rtIds: rtIds && rtIds.length > 0 ? rtIds : undefined,
19782
+ fieldFilter
19783
+ }).then(rows => source.primary
19784
+ ? rows.map(r => this.reshapeResolutionRow(r, label))
19785
+ // The transient row's one value column is named after the reducer (`<field>_min` for MIN …);
19786
+ // re-emit it under the series' value field so no reducer suffix is mistaken for a band.
19787
+ : rows.map(r => ({ ...r, cells: [{ attributePath: source.valueField, value: r.cells.find(c => c.attributePath.toLowerCase() !== 'timestamp')?.value ?? null }] })));
19788
+ if (!source.primary) {
19789
+ // One source = one series: a single call over the whole scope, reduced server-side.
19790
+ return { rows: await downsample(ra.scopeRtIds, source.label), downsampled: true };
19791
+ }
19792
+ const rows = ds.aggregateSeriesByGroup && ra.ckTypeId
19793
+ ? await this.loadGroupAggregatedRows(ra.ckTypeId, ra.scopeRtIds, downsample)
19794
+ : await this.loadPerRtIdRows(ra.ckTypeId, ra.scopeRtIds, downsample);
19795
+ return { rows, downsampled: true };
19796
+ }
19797
+ const scope = this.stateService.resolveStreamDataScope(ds.entitySelectorId, ds.queryFamily, ds.ignoreTimeFilter);
19798
+ const timeArgs = prepared.timeArgs;
19799
+ const downsampled = ds.queryFamily === 'streamData' && !!timeArgs?.from && !!timeArgs?.to;
19800
+ const streamDataArgs = downsampled
19801
+ ? { ...timeArgs, rtIds: prepared.rtIds, limit, queryMode: QueryModeDto.DownsamplingDto }
19802
+ : (timeArgs || prepared.rtIds ? { ...timeArgs, rtIds: prepared.rtIds } : undefined);
19803
+ const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
19804
+ fieldFilter,
19805
+ streamDataArgs,
19806
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
19807
+ }));
19808
+ const rows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
19809
+ return { rows, downsampled };
19810
+ }
19811
+ /** Writes aligned series into the chart signals (multi-source path, AB#7951). */
19812
+ applyAlignedGrid(instants, series) {
19813
+ const mode = this.stateService.timeZoneMode();
19814
+ const days = new Set(instants.map(t => formatInstant(new Date(t), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '?'));
19815
+ const needsTime = days.size < instants.length;
19816
+ const categories = uniqueCategoryLabels(instants, d => needsTime ? this.formatDateTime(d) : this.formatDate(d));
19817
+ const unitMap = new Map();
19818
+ const seriesData = series.map((s, index) => {
19819
+ if (s.unit)
19820
+ unitMap.set(s.name, s.unit);
19821
+ return {
19822
+ name: s.name,
19823
+ data: s.data,
19824
+ unit: s.unit,
19825
+ color: SERIES_PALETTE[index % SERIES_PALETTE.length],
19826
+ stacked: s.stacked,
19827
+ asLine: s.asLine
19828
+ };
19829
+ });
19830
+ this._categories.set(categories);
19831
+ this._seriesData.set(seriesData);
19832
+ // One value axis: series of different queries share it (per-unit axes stay a single-query feature).
19833
+ this._valueAxes.set([]);
19834
+ this._seriesUnitMap.set(unitMap);
19835
+ }
19836
+ /** Short message of a failed source for its hint. */
19837
+ errorMessage(reason) {
19838
+ const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
19839
+ return message ? `Failed to load data (${message})` : 'Failed to load data';
19840
+ }
17893
19841
  buildStreamDataArgs() {
17894
19842
  const ds = this.config.dataSource;
17895
19843
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
@@ -17959,10 +19907,17 @@ class LineChartWidgetComponent {
17959
19907
  lineValue = this.toNumber(cell.value); // _avg (or an unsuffixed value)
17960
19908
  }
17961
19909
  else {
17962
- const val = cell.value;
17963
- lineValue = typeof val === 'number' ? val : parseFloat(String(val));
17964
- if (isNaN(lineValue))
17965
- lineValue = 0;
19910
+ // Exact cell first, so a null `<field>_integral` never overrides a usable `<field>`.
19911
+ const val = (findCellForField(row.cells, valueField) ?? cell).value;
19912
+ if (val === null || val === undefined) {
19913
+ // No data (e.g. an Integral over an empty bucket, AB#7975): no point, not 0.
19914
+ lineValue = null;
19915
+ }
19916
+ else {
19917
+ lineValue = typeof val === 'number' ? val : parseFloat(String(val));
19918
+ if (isNaN(lineValue))
19919
+ lineValue = 0;
19920
+ }
17966
19921
  }
17967
19922
  }
17968
19923
  else if (unitField && matchesAttributePath(path, unitField)) {
@@ -18102,6 +20057,15 @@ class LineChartWidgetComponent {
18102
20057
  </span>
18103
20058
  }
18104
20059
  }
20060
+ @if (sourceHints().length > 0) {
20061
+ <ul class="source-hints" aria-live="polite">
20062
+ @for (hint of sourceHints(); track hint.text) {
20063
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
20064
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
20065
+ </li>
20066
+ }
20067
+ </ul>
20068
+ }
18105
20069
  @if (showResolutionInfo() && resolutionExplanation(); as msg) {
18106
20070
  <div class="resolution-info">
18107
20071
  <div class="ri-head">
@@ -18173,13 +20137,15 @@ class LineChartWidgetComponent {
18173
20137
  </kendo-chart-series-item>
18174
20138
  }
18175
20139
  <kendo-chart-series-item
18176
- [type]="chartType()"
20140
+ [type]="series.asLine ? 'line' : chartType()"
18177
20141
  [style]="'smooth'"
18178
20142
  [data]="series.data"
18179
20143
  [name]="series.name"
18180
20144
  [axis]="series.axisName ?? ''"
18181
20145
  [color]="series.color"
18182
- [opacity]="0.7"
20146
+ [opacity]="series.asLine ? 1 : 0.7"
20147
+ [stack]="series.stacked ?? stackSeries()"
20148
+ [missingValues]="missingValues()"
18183
20149
  [markers]="{ visible: config.showMarkers ?? false }">
18184
20150
  </kendo-chart-series-item>
18185
20151
  }
@@ -18202,9 +20168,16 @@ class LineChartWidgetComponent {
18202
20168
  <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
18203
20169
  <div class="chart-tooltip">
18204
20170
  <strong>{{ category }}</strong>
18205
- @for (point of points; track point.series.name) {
18206
- @if (!isBandSeries(point.series.name)) {
18207
- <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
20171
+ @if (isMultiSource()) {
20172
+ <!-- Multi-source (AB#7952): every drawn series, a gap as "–" (Kendo leaves gap points out of points). -->
20173
+ @for (row of tooltipRows(category); track row.name) {
20174
+ <br/>{{ row.name }}: {{ row.text }}
20175
+ }
20176
+ } @else {
20177
+ @for (point of points; track point.series.name) {
20178
+ @if (!isBandSeries(point.series.name)) {
20179
+ <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
20180
+ }
18208
20181
  }
18209
20182
  }
18210
20183
  </div>
@@ -18213,7 +20186,7 @@ class LineChartWidgetComponent {
18213
20186
  </kendo-chart>
18214
20187
  }
18215
20188
  </div>
18216
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
20189
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18217
20190
  }
18218
20191
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
18219
20192
  type: Component,
@@ -18243,6 +20216,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18243
20216
  </span>
18244
20217
  }
18245
20218
  }
20219
+ @if (sourceHints().length > 0) {
20220
+ <ul class="source-hints" aria-live="polite">
20221
+ @for (hint of sourceHints(); track hint.text) {
20222
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
20223
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
20224
+ </li>
20225
+ }
20226
+ </ul>
20227
+ }
18246
20228
  @if (showResolutionInfo() && resolutionExplanation(); as msg) {
18247
20229
  <div class="resolution-info">
18248
20230
  <div class="ri-head">
@@ -18314,13 +20296,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18314
20296
  </kendo-chart-series-item>
18315
20297
  }
18316
20298
  <kendo-chart-series-item
18317
- [type]="chartType()"
20299
+ [type]="series.asLine ? 'line' : chartType()"
18318
20300
  [style]="'smooth'"
18319
20301
  [data]="series.data"
18320
20302
  [name]="series.name"
18321
20303
  [axis]="series.axisName ?? ''"
18322
20304
  [color]="series.color"
18323
- [opacity]="0.7"
20305
+ [opacity]="series.asLine ? 1 : 0.7"
20306
+ [stack]="series.stacked ?? stackSeries()"
20307
+ [missingValues]="missingValues()"
18324
20308
  [markers]="{ visible: config.showMarkers ?? false }">
18325
20309
  </kendo-chart-series-item>
18326
20310
  }
@@ -18343,9 +20327,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18343
20327
  <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
18344
20328
  <div class="chart-tooltip">
18345
20329
  <strong>{{ category }}</strong>
18346
- @for (point of points; track point.series.name) {
18347
- @if (!isBandSeries(point.series.name)) {
18348
- <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
20330
+ @if (isMultiSource()) {
20331
+ <!-- Multi-source (AB#7952): every drawn series, a gap as "–" (Kendo leaves gap points out of points). -->
20332
+ @for (row of tooltipRows(category); track row.name) {
20333
+ <br/>{{ row.name }}: {{ row.text }}
20334
+ }
20335
+ } @else {
20336
+ @for (point of points; track point.series.name) {
20337
+ @if (!isBandSeries(point.series.name)) {
20338
+ <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
20339
+ }
18349
20340
  }
18350
20341
  }
18351
20342
  </div>
@@ -18354,7 +20345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18354
20345
  </kendo-chart>
18355
20346
  }
18356
20347
  </div>
18357
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
20348
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
18358
20349
  }], propDecorators: { config: [{
18359
20350
  type: Input
18360
20351
  }] } });
@@ -18368,6 +20359,7 @@ class LineChartConfigDialogComponent {
18368
20359
  queryExecutor = inject(QueryExecutorService);
18369
20360
  stateService = inject(MeshBoardStateService);
18370
20361
  windowRef = inject(WindowRef);
20362
+ expressionEvaluator = inject(ExpressionEvaluatorService);
18371
20363
  querySelector;
18372
20364
  // Initial values for editing
18373
20365
  initialQueryRtId;
@@ -18388,6 +20380,13 @@ class LineChartConfigDialogComponent {
18388
20380
  initialShowDataBadge;
18389
20381
  initialReferenceLines;
18390
20382
  initialFilters;
20383
+ initialAdditionalSeries;
20384
+ initialStacked;
20385
+ initialPrimarySeries;
20386
+ /** Query and formula series of the Series section (AB#7951, AB#7952). */
20387
+ additionalSeries = [];
20388
+ /** Display options of the primary query's series (AB#7952). */
20389
+ primarySeries;
18391
20390
  // State
18392
20391
  isLoadingInitial = false;
18393
20392
  isLoadingColumns = false;
@@ -18442,13 +20441,19 @@ class LineChartConfigDialogComponent {
18442
20441
  legendPosition: 'right',
18443
20442
  showMarkers: false,
18444
20443
  showDataBadge: true,
20444
+ stacked: false,
18445
20445
  referenceLines: []
18446
20446
  };
18447
20447
  get isValid() {
18448
20448
  return this.selectedPersistentQuery !== null
18449
20449
  && this.form.categoryField !== ''
18450
20450
  && this.form.seriesGroupField !== ''
18451
- && this.form.valueField !== '';
20451
+ && this.form.valueField !== ''
20452
+ && querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator).every(e => e === null);
20453
+ }
20454
+ /** The query series that are saved: none unless the primary query is a stream-data query. */
20455
+ get effectiveAdditionalSeries() {
20456
+ return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
18452
20457
  }
18453
20458
  async ngOnInit() {
18454
20459
  // Initialize filter variables from MeshBoard state
@@ -18472,6 +20477,9 @@ class LineChartConfigDialogComponent {
18472
20477
  this.form.showMarkers = this.initialShowMarkers ?? false;
18473
20478
  this.form.showDataBadge = this.initialShowDataBadge ?? true;
18474
20479
  this.form.referenceLines = this.initialReferenceLines ? [...this.initialReferenceLines] : [];
20480
+ this.form.stacked = this.initialStacked ?? false;
20481
+ this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
20482
+ this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
18475
20483
  // Initialize filters
18476
20484
  if (this.initialFilters && this.initialFilters.length > 0) {
18477
20485
  this.filters = this.initialFilters.map((f, index) => ({
@@ -18553,8 +20561,10 @@ class LineChartConfigDialogComponent {
18553
20561
  aggregationType: c.aggregationType ?? null
18554
20562
  }));
18555
20563
  }
18556
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
18557
- return sdResult.columns.map(c => ({
20564
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
20565
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
20566
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
20567
+ return columns.map(c => ({
18558
20568
  attributePath: c.attributePath,
18559
20569
  attributeValueType: c.attributeValueType ?? '',
18560
20570
  aggregationType: c.aggregationType ?? null
@@ -18594,7 +20604,10 @@ class LineChartConfigDialogComponent {
18594
20604
  showMarkers: this.form.showMarkers,
18595
20605
  showDataBadge: this.form.showDataBadge,
18596
20606
  referenceLines: this.form.referenceLines.length > 0 ? this.form.referenceLines : undefined,
18597
- filters: filtersDto
20607
+ filters: filtersDto,
20608
+ additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
20609
+ stacked: this.form.chartType === 'area' && this.form.stacked ? true : undefined,
20610
+ primarySeries: this.selectedQueryFamily === 'streamData' ? this.primarySeries : undefined
18598
20611
  };
18599
20612
  this.windowRef.close(result);
18600
20613
  }
@@ -18608,7 +20621,7 @@ class LineChartConfigDialogComponent {
18608
20621
  this.windowRef.close();
18609
20622
  }
18610
20623
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18611
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
20624
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialStacked: "initialStacked", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
18612
20625
  <div class="config-container">
18613
20626
 
18614
20627
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -18749,6 +20762,22 @@ class LineChartConfigDialogComponent {
18749
20762
  </div>
18750
20763
  }
18751
20764
 
20765
+ <!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
20766
+ @if (selectedQueryFamily === 'streamData') {
20767
+ <div class="config-section">
20768
+ <h3 class="section-title">Series</h3>
20769
+ <p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
20770
+ <mm-line-chart-series-editor
20771
+ [series]="additionalSeries"
20772
+ [selectors]="availableEntitySelectors"
20773
+ [primary]="primarySeries"
20774
+ [stackedChart]="form.chartType === 'area' && form.stacked"
20775
+ (seriesChange)="additionalSeries = $event"
20776
+ (primaryChange)="primarySeries = $event">
20777
+ </mm-line-chart-series-editor>
20778
+ </div>
20779
+ }
20780
+
18752
20781
  <!-- Filters Section -->
18753
20782
  @if (selectedPersistentQuery?.queryCkTypeId) {
18754
20783
  <div class="config-section">
@@ -18790,6 +20819,16 @@ class LineChartConfigDialogComponent {
18790
20819
  </div>
18791
20820
  </div>
18792
20821
 
20822
+ @if (form.chartType === 'area') {
20823
+ <div class="form-field checkbox-field">
20824
+ <label>
20825
+ <input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
20826
+ Stacked
20827
+ </label>
20828
+ </div>
20829
+ <p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
20830
+ }
20831
+
18793
20832
  <div class="form-row">
18794
20833
  <div class="form-field checkbox-field">
18795
20834
  <label>
@@ -18853,7 +20892,7 @@ class LineChartConfigDialogComponent {
18853
20892
  </button>
18854
20893
  </div>
18855
20894
  </div>
18856
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
20895
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18857
20896
  }
18858
20897
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
18859
20898
  type: Component,
@@ -18868,7 +20907,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18868
20907
  QuerySelectorComponent,
18869
20908
  SdTimeFilterToggleComponent,
18870
20909
  EntitySelectorScopePickerComponent,
18871
- LoadingOverlayComponent
20910
+ LoadingOverlayComponent,
20911
+ LineChartSeriesEditorComponent
18872
20912
  ], template: `
18873
20913
  <div class="config-container">
18874
20914
 
@@ -19010,6 +21050,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19010
21050
  </div>
19011
21051
  }
19012
21052
 
21053
+ <!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
21054
+ @if (selectedQueryFamily === 'streamData') {
21055
+ <div class="config-section">
21056
+ <h3 class="section-title">Series</h3>
21057
+ <p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
21058
+ <mm-line-chart-series-editor
21059
+ [series]="additionalSeries"
21060
+ [selectors]="availableEntitySelectors"
21061
+ [primary]="primarySeries"
21062
+ [stackedChart]="form.chartType === 'area' && form.stacked"
21063
+ (seriesChange)="additionalSeries = $event"
21064
+ (primaryChange)="primarySeries = $event">
21065
+ </mm-line-chart-series-editor>
21066
+ </div>
21067
+ }
21068
+
19013
21069
  <!-- Filters Section -->
19014
21070
  @if (selectedPersistentQuery?.queryCkTypeId) {
19015
21071
  <div class="config-section">
@@ -19051,6 +21107,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19051
21107
  </div>
19052
21108
  </div>
19053
21109
 
21110
+ @if (form.chartType === 'area') {
21111
+ <div class="form-field checkbox-field">
21112
+ <label>
21113
+ <input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
21114
+ Stacked
21115
+ </label>
21116
+ </div>
21117
+ <p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
21118
+ }
21119
+
19054
21120
  <div class="form-row">
19055
21121
  <div class="form-field checkbox-field">
19056
21122
  <label>
@@ -19154,6 +21220,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19154
21220
  type: Input
19155
21221
  }], initialFilters: [{
19156
21222
  type: Input
21223
+ }], initialAdditionalSeries: [{
21224
+ type: Input
21225
+ }], initialStacked: [{
21226
+ type: Input
21227
+ }], initialPrimarySeries: [{
21228
+ type: Input
19157
21229
  }] } });
19158
21230
 
19159
21231
  /**
@@ -19520,6 +21592,22 @@ class HeatmapWidgetComponent {
19520
21592
  numericValue = 0;
19521
21593
  }
19522
21594
  }
21595
+ // A null value cell is "no data" (e.g. an Integral over an empty window, AB#7975): the
21596
+ // row is skipped instead of counting as 0 in its bucket (sum/avg). The value cell is
21597
+ // resolved exactly (`findCellForField`), so a null `<field>_integral` never hides or
21598
+ // overwrites a usable `<field>`.
21599
+ // A count ignores the value; a weighted mean validates value and weight below.
21600
+ if (valueField && aggregation !== 'count' && !weighted) {
21601
+ const valueCell = findCellForField(row.cells, valueField);
21602
+ if (valueCell) {
21603
+ const val = valueCell.value;
21604
+ if (val === null || val === undefined)
21605
+ continue;
21606
+ numericValue = typeof val === 'number' ? val : parseFloat(String(val));
21607
+ if (isNaN(numericValue))
21608
+ numericValue = 0;
21609
+ }
21610
+ }
19523
21611
  // Stream-data rows carry their time at top level (`row.timestamp`, see
19524
21612
  // QueryExecutorService.mapStreamDataRows); the cells only hold the value
19525
21613
  // columns. Same source the line chart uses for the bin time (AB#7938).
@@ -20152,8 +22240,10 @@ class HeatmapConfigDialogComponent {
20152
22240
  aggregationType: c.aggregationType ?? null
20153
22241
  }));
20154
22242
  }
20155
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
20156
- return sdResult.columns.map(c => ({
22243
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
22244
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
22245
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
22246
+ return columns.map(c => ({
20157
22247
  attributePath: c.attributePath,
20158
22248
  attributeValueType: c.attributeValueType ?? '',
20159
22249
  aggregationType: c.aggregationType ?? null
@@ -23180,8 +25270,10 @@ class WidgetGroupConfigDialogComponent {
23180
25270
  aggregationType: c.aggregationType ?? null
23181
25271
  }));
23182
25272
  }
23183
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
23184
- return sdResult.columns.map(c => ({
25273
+ // An Integral column needs from/to (AB#7921): the executor retries with the board's
25274
+ // window, or the last 24 hours when the board has no time filter (AB#7975).
25275
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
25276
+ return columns.map(c => ({
23185
25277
  attributePath: c.attributePath,
23186
25278
  attributeValueType: c.attributeValueType ?? '',
23187
25279
  aggregationType: c.aggregationType ?? null
@@ -26023,6 +28115,332 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
26023
28115
  `, changeDetection: ChangeDetectionStrategy.Eager, styles: [".config-form{display:flex;flex-direction:column;gap:12px;padding:16px}.form-group{display:flex;flex-direction:column;gap:4px}.form-row{display:flex;gap:16px}.form-row .form-group{flex:1}.hint{font-size:.75rem;opacity:.6}\n"] }]
26024
28116
  }] });
26025
28117
 
28118
+ /**
28119
+ * Process Widget Type identifier
28120
+ */
28121
+ const PROCESS_WIDGET_TYPE = 'process';
28122
+ /**
28123
+ * Name of the implicit binding a config without `bindings` is read as: the
28124
+ * legacy single-binding fields (`dataBindingMode`, `bindingCkTypeId`, …).
28125
+ * Mappings without `bindingName` refer to it.
28126
+ */
28127
+ const DEFAULT_PROCESS_BINDING_NAME = 'default';
28128
+ /**
28129
+ * Default configuration for a new Process Widget
28130
+ */
28131
+ const DEFAULT_PROCESS_WIDGET_CONFIG = {
28132
+ type: 'process',
28133
+ fitToBounds: true,
28134
+ allowZoom: false,
28135
+ allowPan: false,
28136
+ showToolbar: false,
28137
+ initialZoom: 1
28138
+ };
28139
+
28140
+ /**
28141
+ * Aggregation of a mapped Process Widget value over all rows of a query
28142
+ * binding's selector scope (AB#8024): `first` (default, the first row as
28143
+ * before), `sum`, `avg`, `min`, `max`.
28144
+ *
28145
+ * Aggregation works on **query bindings only**: a runtime-entity binding loads
28146
+ * a single entity, so there is nothing to aggregate. To aggregate over all
28147
+ * entities of a scope, bind a runtime persistent query with the entity
28148
+ * selector instead.
28149
+ */
28150
+ /** All aggregates a mapping may name. */
28151
+ const PROCESS_VALUE_AGGREGATES = ['first', 'sum', 'avg', 'min', 'max'];
28152
+ /** `true` when the aggregate reduces several rows (anything but `first`/absent). */
28153
+ function isReducingAggregate(aggregate) {
28154
+ return aggregate !== undefined && aggregate !== 'first';
28155
+ }
28156
+ /**
28157
+ * Reads one cell of a query row: the column name, or its sanitized form
28158
+ * (dots → underscores) as the query service delivers nested paths.
28159
+ */
28160
+ function cellValue(cells, columnName) {
28161
+ return cells.get(columnName) ?? cells.get(columnName.replace(/\./g, '_'));
28162
+ }
28163
+ /** A finite number from a cell, or `undefined` for null/undefined/non-numeric cells. */
28164
+ function numericCell(value) {
28165
+ if (typeof value === 'number') {
28166
+ return Number.isFinite(value) ? value : undefined;
28167
+ }
28168
+ if (typeof value === 'string' && value.trim() !== '') {
28169
+ const parsed = Number(value);
28170
+ return Number.isFinite(parsed) ? parsed : undefined;
28171
+ }
28172
+ return undefined;
28173
+ }
28174
+ /**
28175
+ * The value of a column over the query rows.
28176
+ *
28177
+ * - `first` / absent: the cell of the first row, unchanged (today's behaviour).
28178
+ * - `sum`, `avg`, `min`, `max`: over the numeric cells; null, undefined and
28179
+ * non-numeric cells are skipped. Zero rows or no numeric cell ⇒ `undefined`
28180
+ * (the property keeps no value, not 0).
28181
+ */
28182
+ function aggregateQueryColumn(queryResult, columnName, aggregate) {
28183
+ const rows = queryResult.rows ?? [];
28184
+ if (rows.length === 0) {
28185
+ return undefined;
28186
+ }
28187
+ if (!isReducingAggregate(aggregate)) {
28188
+ return cellValue(rows[0].cells, columnName);
28189
+ }
28190
+ const numbers = [];
28191
+ for (const row of rows) {
28192
+ const n = numericCell(cellValue(row.cells, columnName));
28193
+ if (n !== undefined) {
28194
+ numbers.push(n);
28195
+ }
28196
+ }
28197
+ if (numbers.length === 0) {
28198
+ return undefined;
28199
+ }
28200
+ switch (aggregate) {
28201
+ case 'sum':
28202
+ return numbers.reduce((a, b) => a + b, 0);
28203
+ case 'avg':
28204
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
28205
+ case 'min':
28206
+ return Math.min(...numbers);
28207
+ case 'max':
28208
+ return Math.max(...numbers);
28209
+ default:
28210
+ return undefined;
28211
+ }
28212
+ }
28213
+ /**
28214
+ * `true` when a reducing aggregate ran over fewer rows than the scope holds
28215
+ * (`totalCount` > loaded rows) — the result is partial and worth a warning.
28216
+ */
28217
+ function isPartialAggregate(queryResult) {
28218
+ const loaded = queryResult.rows?.length ?? 0;
28219
+ return typeof queryResult.totalCount === 'number' && queryResult.totalCount > loaded;
28220
+ }
28221
+ /**
28222
+ * Why a mapping's aggregate cannot apply, or `undefined` when it can (or is
28223
+ * `first`/absent). A reducing aggregate needs a `number` property and a
28224
+ * persistent-query binding. The widget falls back to `first` in those cases.
28225
+ *
28226
+ * @param propertyType Type of the diagram's transform property, when known.
28227
+ * @param bindingMode Mode of the mapping's binding, when known.
28228
+ */
28229
+ function processAggregateIssue(aggregate, propertyType, bindingMode) {
28230
+ if (aggregate === undefined) {
28231
+ return undefined;
28232
+ }
28233
+ if (!PROCESS_VALUE_AGGREGATES.includes(aggregate)) {
28234
+ return `Unknown aggregate '${String(aggregate)}'`;
28235
+ }
28236
+ if (!isReducingAggregate(aggregate)) {
28237
+ return undefined;
28238
+ }
28239
+ if (bindingMode === 'runtimeEntity') {
28240
+ return `Aggregate '${aggregate}' needs a persistent query binding; a runtime entity binding loads one entity`;
28241
+ }
28242
+ if (propertyType !== undefined && propertyType !== 'number') {
28243
+ return `Aggregate '${aggregate}' needs a number property, not '${propertyType}'`;
28244
+ }
28245
+ return undefined;
28246
+ }
28247
+
28248
+ /**
28249
+ * The legacy single-binding fields read as one binding named
28250
+ * {@link DEFAULT_PROCESS_BINDING_NAME}, or `undefined` when the legacy mode is
28251
+ * absent or `'none'`.
28252
+ */
28253
+ function legacyProcessBinding(config) {
28254
+ const mode = config.dataBindingMode;
28255
+ if (!mode || mode === 'none') {
28256
+ return undefined;
28257
+ }
28258
+ return {
28259
+ name: DEFAULT_PROCESS_BINDING_NAME,
28260
+ mode,
28261
+ ckTypeId: config.bindingCkTypeId,
28262
+ rtId: config.bindingRtId,
28263
+ queryRtId: config.bindingQueryRtId,
28264
+ queryName: config.bindingQueryName,
28265
+ queryFamily: config.bindingQueryFamily,
28266
+ ignoreTimeFilter: config.bindingIgnoreTimeFilter,
28267
+ entitySelectorId: config.bindingEntitySelectorId,
28268
+ filters: config.bindingFilters
28269
+ };
28270
+ }
28271
+ /** `true` when the config uses the named `bindings` list instead of the legacy fields. */
28272
+ function hasNamedProcessBindings(config) {
28273
+ return Array.isArray(config.bindings) && config.bindings.length > 0;
28274
+ }
28275
+ /**
28276
+ * The bindings the widget loads: the named `bindings` when present, otherwise
28277
+ * the implicit legacy binding (or none).
28278
+ */
28279
+ function resolveProcessBindings(config) {
28280
+ if (hasNamedProcessBindings(config)) {
28281
+ return config.bindings;
28282
+ }
28283
+ const legacy = legacyProcessBinding(config);
28284
+ return legacy ? [legacy] : [];
28285
+ }
28286
+ /** The binding name a mapping reads from (`default` when absent). */
28287
+ function mappingBindingName(mapping) {
28288
+ return mapping.bindingName || DEFAULT_PROCESS_BINDING_NAME;
28289
+ }
28290
+ /**
28291
+ * The config problem of a single binding, or `undefined` when it can load.
28292
+ * A runtime-entity binding needs a CK type (or a selector to take it from) and
28293
+ * an rtId or an entity selector; a query binding needs a query rtId.
28294
+ */
28295
+ function processBindingConfigError(binding) {
28296
+ if (binding.mode === 'runtimeEntity') {
28297
+ if (!binding.rtId && !binding.entitySelectorId) {
28298
+ return 'Runtime entity binding needs an rtId or an entity selector';
28299
+ }
28300
+ if (!binding.ckTypeId && !binding.entitySelectorId) {
28301
+ return 'Runtime entity binding needs a CK type';
28302
+ }
28303
+ return undefined;
28304
+ }
28305
+ if (binding.mode === 'persistentQuery') {
28306
+ return binding.queryRtId ? undefined : 'Persistent query binding needs a query rtId';
28307
+ }
28308
+ return `Unknown binding mode '${String(binding.mode)}'`;
28309
+ }
28310
+ /**
28311
+ * Validates the bindings and mappings of a Process Widget config (AB#8023):
28312
+ * empty or duplicate binding names, bindings that cannot load (see
28313
+ * {@link processBindingConfigError}), selectors that do not exist on the board
28314
+ * (only checked when `knownSelectorIds` is given), and mappings that refer to an
28315
+ * unknown binding. A legacy config is validated like its implicit binding; a
28316
+ * config without any binding has no issues.
28317
+ *
28318
+ * Aggregates (AB#8024): an unknown aggregate, or one other than `first` on a
28319
+ * runtime-entity binding, an `attribute` mapping or — when `propertyTypes` is
28320
+ * given — a non-`number` property, is reported; the widget then reads the
28321
+ * first row.
28322
+ *
28323
+ * @param propertyTypes Type per diagram property id (`transformProperties`), when the diagram is known.
28324
+ */
28325
+ function validateProcessWidgetBindings(config, knownSelectorIds, propertyTypes) {
28326
+ const issues = [];
28327
+ const bindings = resolveProcessBindings(config);
28328
+ const names = new Set();
28329
+ for (const binding of bindings) {
28330
+ if (!binding.name || !binding.name.trim()) {
28331
+ issues.push({ message: 'A binding has no name' });
28332
+ continue;
28333
+ }
28334
+ if (names.has(binding.name)) {
28335
+ issues.push({ bindingName: binding.name, message: `Duplicate binding name '${binding.name}'` });
28336
+ }
28337
+ names.add(binding.name);
28338
+ const error = processBindingConfigError(binding);
28339
+ if (error) {
28340
+ issues.push({ bindingName: binding.name, message: error });
28341
+ }
28342
+ if (binding.entitySelectorId && knownSelectorIds && !knownSelectorIds.includes(binding.entitySelectorId)) {
28343
+ issues.push({
28344
+ bindingName: binding.name,
28345
+ message: `Entity selector '${binding.entitySelectorId}' does not exist on the board`
28346
+ });
28347
+ }
28348
+ }
28349
+ // Only report unknown names when the widget has bindings at all — a widget
28350
+ // without data binding keeps stale mappings silently, as before.
28351
+ if (bindings.length > 0) {
28352
+ for (const mapping of config.propertyMappings ?? []) {
28353
+ const name = mappingBindingName(mapping);
28354
+ if (!names.has(name)) {
28355
+ issues.push({
28356
+ bindingName: name,
28357
+ propertyId: mapping.propertyId,
28358
+ message: `Mapping of '${mapping.propertyId}' refers to unknown binding '${name}'`
28359
+ });
28360
+ }
28361
+ const aggregateIssue = mappingAggregateIssue(mapping, bindings.find(b => b.name === name), propertyTypes);
28362
+ if (aggregateIssue) {
28363
+ issues.push({
28364
+ bindingName: name,
28365
+ propertyId: mapping.propertyId,
28366
+ message: `Mapping of '${mapping.propertyId}': ${aggregateIssue}; the first row is used`
28367
+ });
28368
+ }
28369
+ }
28370
+ }
28371
+ return issues;
28372
+ }
28373
+ /**
28374
+ * Why a mapping's `aggregate` cannot apply (AB#8024), or `undefined`. Besides
28375
+ * {@link processAggregateIssue} an `attribute` mapping cannot aggregate: it
28376
+ * reads one entity.
28377
+ */
28378
+ function mappingAggregateIssue(mapping, binding, propertyTypes) {
28379
+ const issue = processAggregateIssue(mapping.aggregate, propertyTypes?.get(mapping.propertyId), binding?.mode);
28380
+ if (issue) {
28381
+ return issue;
28382
+ }
28383
+ if (mapping.aggregate && mapping.aggregate !== 'first' && mapping.sourceType === 'attribute') {
28384
+ return `Aggregate '${mapping.aggregate}' needs a column mapping; an attribute mapping reads one entity`;
28385
+ }
28386
+ return undefined;
28387
+ }
28388
+ /**
28389
+ * Reads persisted `bindings` (JSON/YAML seed) into typed bindings. Entries that
28390
+ * are not objects or have an unknown mode are dropped; returns `undefined` when
28391
+ * nothing usable is left, so a config without bindings stays without the key.
28392
+ */
28393
+ function parsePersistedProcessBindings(raw) {
28394
+ if (!Array.isArray(raw)) {
28395
+ return undefined;
28396
+ }
28397
+ const bindings = [];
28398
+ for (const entry of raw) {
28399
+ if (!entry || typeof entry !== 'object') {
28400
+ continue;
28401
+ }
28402
+ const e = entry;
28403
+ if (e['mode'] !== 'runtimeEntity' && e['mode'] !== 'persistentQuery') {
28404
+ console.warn('[ProcessWidget] Ignoring binding with unknown mode:', e['name'], e['mode']);
28405
+ continue;
28406
+ }
28407
+ bindings.push(compactBinding({
28408
+ name: typeof e['name'] === 'string' ? e['name'] : '',
28409
+ mode: e['mode'],
28410
+ ckTypeId: e['ckTypeId'],
28411
+ rtId: e['rtId'],
28412
+ queryRtId: e['queryRtId'],
28413
+ queryName: e['queryName'],
28414
+ queryFamily: e['queryFamily'],
28415
+ ignoreTimeFilter: e['ignoreTimeFilter'],
28416
+ entitySelectorId: e['entitySelectorId'],
28417
+ filters: e['filters']
28418
+ }));
28419
+ }
28420
+ return bindings.length > 0 ? bindings : undefined;
28421
+ }
28422
+ /**
28423
+ * Serializes bindings for persistence: only set keys are written
28424
+ * (`ignoreTimeFilter` only when `true`, `filters` only when non-empty).
28425
+ */
28426
+ function toPersistedProcessBindings(bindings) {
28427
+ return bindings.map(compactBinding);
28428
+ }
28429
+ function compactBinding(binding) {
28430
+ return {
28431
+ name: binding.name,
28432
+ mode: binding.mode,
28433
+ ...(binding.ckTypeId && { ckTypeId: binding.ckTypeId }),
28434
+ ...(binding.rtId && { rtId: binding.rtId }),
28435
+ ...(binding.queryRtId && { queryRtId: binding.queryRtId }),
28436
+ ...(binding.queryName && { queryName: binding.queryName }),
28437
+ ...(binding.queryFamily && { queryFamily: binding.queryFamily }),
28438
+ ...(binding.ignoreTimeFilter === true && { ignoreTimeFilter: true }),
28439
+ ...(binding.entitySelectorId && { entitySelectorId: binding.entitySelectorId }),
28440
+ ...(binding.filters && binding.filters.length > 0 && { filters: binding.filters })
28441
+ };
28442
+ }
28443
+
26026
28444
  class ProcessDataService {
26027
28445
  dataService = inject(MeshBoardDataService);
26028
28446
  variableService = inject(MeshBoardVariableService);
@@ -26488,51 +28906,90 @@ class ProcessDataService {
26488
28906
  // Widget Data Binding Methods
26489
28907
  // ============================================================================
26490
28908
  /**
26491
- * Loads bound data based on widget configuration.
26492
- * Supports both runtimeEntity and persistentQuery data binding modes.
28909
+ * Loads data based on the widget's legacy single-binding configuration —
28910
+ * the implicit binding named `default` (see {@link legacyProcessBinding}).
26493
28911
  *
26494
28912
  * @param config - The process widget configuration with data binding settings
26495
28913
  * @returns BoundDataResult containing the loaded data, or null if no binding configured
26496
28914
  */
26497
28915
  async loadBoundData(config) {
26498
- if (!config.dataBindingMode || config.dataBindingMode === 'none') {
26499
- return null;
26500
- }
26501
- try {
26502
- if (config.dataBindingMode === 'runtimeEntity') {
26503
- return await this.loadBoundRuntimeEntityData(config);
28916
+ const binding = legacyProcessBinding(config);
28917
+ return binding ? this.loadBinding(binding) : null;
28918
+ }
28919
+ /**
28920
+ * Loads every binding of the widget in parallel (AB#8023): the named
28921
+ * `bindings`, or the implicit legacy binding `default`. One result per
28922
+ * binding name (first binding wins on a duplicate name). A failing binding
28923
+ * yields a result with `error`, a binding whose selector scope is still
28924
+ * resolving a result with `pending` — neither blocks the others.
28925
+ */
28926
+ async loadBindings(config) {
28927
+ const bindings = resolveProcessBindings(config);
28928
+ const results = await Promise.all(bindings.map(binding => this.loadBinding(binding)));
28929
+ const byName = new Map();
28930
+ bindings.forEach((binding, index) => {
28931
+ if (!byName.has(binding.name)) {
28932
+ byName.set(binding.name, results[index]);
26504
28933
  }
26505
- if (config.dataBindingMode === 'persistentQuery') {
26506
- return await this.loadBoundQueryData(config);
28934
+ });
28935
+ return byName;
28936
+ }
28937
+ /**
28938
+ * Loads one binding. Never throws: errors are returned as `error` result.
28939
+ */
28940
+ async loadBinding(binding) {
28941
+ const type = binding.mode === 'runtimeEntity' ? 'entity' : 'query';
28942
+ try {
28943
+ const configError = processBindingConfigError(binding);
28944
+ if (configError) {
28945
+ throw new Error(configError);
26507
28946
  }
26508
- return null;
28947
+ const result = binding.mode === 'runtimeEntity'
28948
+ ? await this.loadBoundRuntimeEntityData(binding)
28949
+ : await this.loadBoundQueryData(binding);
28950
+ return { ...result, bindingName: binding.name };
26509
28951
  }
26510
28952
  catch (error) {
26511
- console.error('[ProcessDataService] Error loading bound data:', error);
28953
+ console.error(`[ProcessDataService] Error loading binding '${binding.name}':`, error);
26512
28954
  return {
26513
- type: config.dataBindingMode === 'runtimeEntity' ? 'entity' : 'query',
28955
+ type,
28956
+ bindingName: binding.name,
26514
28957
  error: error instanceof Error ? error.message : 'Unknown error',
26515
28958
  loadedAt: new Date()
26516
28959
  };
26517
28960
  }
26518
28961
  }
26519
28962
  /**
26520
- * Loads data from a runtime entity binding
28963
+ * Loads data from a runtime entity binding. A fixed `rtId` (variables
28964
+ * resolved) wins; without one the current entity of the bound selector is
28965
+ * loaded (AB#8023): the first rtId of its resolved scope. A pending scope
28966
+ * gives a `pending` result, an empty scope or a selector without pick no
28967
+ * entity (no value, never the previous one).
26521
28968
  */
26522
- async loadBoundRuntimeEntityData(config) {
26523
- const { bindingCkTypeId, bindingRtId } = config;
26524
- if (!bindingCkTypeId) {
26525
- throw new Error('Runtime entity binding requires a CK Type ID');
28969
+ async loadBoundRuntimeEntityData(binding) {
28970
+ let rtId;
28971
+ if (binding.rtId) {
28972
+ // Resolve variables in rtId (e.g., ${selectedEntity})
28973
+ rtId = this.resolveVariables(binding.rtId);
26526
28974
  }
26527
- if (!bindingRtId) {
26528
- throw new Error('Runtime entity binding requires a Runtime ID');
28975
+ else {
28976
+ const scope = this.stateService.resolveEntitySelectorScope(binding.entitySelectorId);
28977
+ if (scope.kind === 'pending') {
28978
+ return { type: 'entity', pending: true, loadedAt: new Date() };
28979
+ }
28980
+ if (scope.kind !== 'scoped') {
28981
+ return { type: 'entity', loadedAt: new Date() };
28982
+ }
28983
+ rtId = scope.rtIds[0];
28984
+ }
28985
+ const ckTypeId = binding.ckTypeId ?? this.selectorCkTypeId(binding.entitySelectorId);
28986
+ if (!ckTypeId) {
28987
+ throw new Error('Runtime entity binding requires a CK Type ID');
26529
28988
  }
26530
- // Resolve variables in rtId (e.g., ${selectedEntity})
26531
- const resolvedRtId = this.resolveVariables(bindingRtId);
26532
28989
  // Fetch the entity
26533
- const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(resolvedRtId, bindingCkTypeId));
28990
+ const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(rtId, ckTypeId));
26534
28991
  if (!entity) {
26535
- throw new Error(`Entity not found: ${resolvedRtId}`);
28992
+ throw new Error(`Entity not found: ${rtId}`);
26536
28993
  }
26537
28994
  return {
26538
28995
  type: 'entity',
@@ -26540,39 +28997,50 @@ class ProcessDataService {
26540
28997
  loadedAt: new Date()
26541
28998
  };
26542
28999
  }
29000
+ /**
29001
+ * CK type of the entity a selector's scope consists of — known only for a
29002
+ * selector without `childScope` (its scope is the picked entity itself).
29003
+ */
29004
+ selectorCkTypeId(selectorId) {
29005
+ if (!selectorId) {
29006
+ return undefined;
29007
+ }
29008
+ const selector = this.stateService.getConfig().entitySelectors?.find(s => s.id === selectorId);
29009
+ return selector && !selector.childScope ? selector.ckTypeId : undefined;
29010
+ }
26543
29011
  /**
26544
29012
  * Loads data from a persistent query binding
26545
29013
  */
26546
- async loadBoundQueryData(config) {
26547
- const { bindingQueryRtId, bindingFilters, bindingQueryFamily } = config;
26548
- if (!bindingQueryRtId) {
29014
+ async loadBoundQueryData(binding) {
29015
+ const { queryRtId, filters, queryFamily } = binding;
29016
+ if (!queryRtId) {
26549
29017
  throw new Error('Persistent query binding requires a Query rtId');
26550
29018
  }
26551
29019
  // Entity-selector scope (AB#7721): NO query is sent while the scope is pending
26552
29020
  // or empty — never the persisted (unscoped) scope. Pending → a `pending` result
26553
29021
  // (the widget stays loading; a refresh follows once the scope resolves);
26554
29022
  // empty → no rows.
26555
- const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily, config.bindingIgnoreTimeFilter);
29023
+ const scope = this.stateService.resolveStreamDataScope(binding.entitySelectorId, queryFamily, binding.ignoreTimeFilter);
26556
29024
  if (scope.kind === 'pending') {
26557
29025
  return { type: 'query', pending: true, loadedAt: new Date() };
26558
29026
  }
26559
29027
  if (scope.kind === 'empty') {
26560
29028
  return {
26561
29029
  type: 'query',
26562
- queryResult: this.mapExecutionResult(emptyQueryExecutionResult(bindingQueryFamily)),
29030
+ queryResult: this.mapExecutionResult(emptyQueryExecutionResult(queryFamily)),
26563
29031
  loadedAt: new Date()
26564
29032
  };
26565
29033
  }
26566
29034
  // Convert widget filters to GraphQL format with variable resolution
26567
- const fieldFilters = this.convertFiltersToDto(bindingFilters);
29035
+ const fieldFilters = this.convertFiltersToDto(filters);
26568
29036
  // Execute via the unified executor — it routes runtime vs stream-data from
26569
29037
  // the persisted family (or a one-time lookup when absent). Stream-data
26570
29038
  // queries additionally consume the time-filter / asset-scope overrides built
26571
29039
  // below; runtime queries ignore `streamDataArgs`.
26572
- const result = await firstValueFrom(this.queryExecutor.execute(bindingQueryFamily, bindingQueryRtId, {
29040
+ const result = await firstValueFrom(this.queryExecutor.execute(queryFamily, queryRtId, {
26573
29041
  first: 1000, // Reasonable limit for process diagram data
26574
29042
  fieldFilter: fieldFilters,
26575
- streamDataArgs: this.buildBindingStreamDataArgs(config),
29043
+ streamDataArgs: this.buildBindingStreamDataArgs(binding),
26576
29044
  // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
26577
29045
  scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
26578
29046
  }));
@@ -26588,9 +29056,9 @@ class ProcessDataService {
26588
29056
  * Returns `undefined` when neither override applies (e.g. runtime query, no
26589
29057
  * time filter, opted out). Runtime queries ignore the result.
26590
29058
  */
26591
- buildBindingStreamDataArgs(config) {
26592
- const timeArgs = this.stateService.resolveStreamDataTimeArgs(config.bindingIgnoreTimeFilter);
26593
- const rtIds = this.stateService.resolveStreamDataRtIds(config.bindingEntitySelectorId);
29059
+ buildBindingStreamDataArgs(binding) {
29060
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(binding.ignoreTimeFilter);
29061
+ const rtIds = this.stateService.resolveStreamDataRtIds(binding.entitySelectorId);
26594
29062
  if (!timeArgs && !rtIds) {
26595
29063
  return undefined;
26596
29064
  }
@@ -26668,9 +29136,12 @@ class ProcessWidgetComponent {
26668
29136
  config;
26669
29137
  containerRef;
26670
29138
  dataService = inject(ProcessDataService);
29139
+ stateService = inject(MeshBoardStateService);
26671
29140
  symbolLibraryService = inject(SymbolLibraryService);
26672
29141
  sanitizer = inject(DomSanitizer);
26673
29142
  expressionEvaluator = inject(ExpressionEvaluatorService);
29143
+ /** Host language for `text.value` bindings (AB#8022); no language = de-AT. */
29144
+ hostLanguage = injectMeshBoardLanguage();
26674
29145
  // Math reference for template
26675
29146
  Math = Math;
26676
29147
  // State signals
@@ -26686,6 +29157,16 @@ class ProcessWidgetComponent {
26686
29157
  ...(ngDevMode ? [{ debugName: "_symbolDefinitions" }] : /* istanbul ignore next */ []));
26687
29158
  _boundData = signal(null, /* @ts-ignore */
26688
29159
  ...(ngDevMode ? [{ debugName: "_boundData" }] : /* istanbul ignore next */ []));
29160
+ /** Loaded results per binding name (AB#8023); pending bindings are absent. */
29161
+ _boundDataByBinding = signal(new Map(), /* @ts-ignore */
29162
+ ...(ngDevMode ? [{ debugName: "_boundDataByBinding" }] : /* istanbul ignore next */ []));
29163
+ /** Console messages already written by this widget instance (each is reported once). */
29164
+ reportedWarnings = new Set();
29165
+ /**
29166
+ * Generation of the latest `loadDiagram()` call: a slower, older load (e.g. of
29167
+ * the previous leader pick) never overwrites the bound data of a newer one.
29168
+ */
29169
+ loadGeneration = 0;
26689
29170
  isLoading = this._isLoading.asReadonly();
26690
29171
  error = this._error.asReadonly();
26691
29172
  diagramConfig = this._diagramConfig.asReadonly();
@@ -26696,6 +29177,30 @@ class ProcessWidgetComponent {
26696
29177
  * Contains entity or query data that can be used for dynamic values in the diagram.
26697
29178
  */
26698
29179
  boundData = this._boundData.asReadonly();
29180
+ /** Loaded results per binding name (AB#8023). Pending bindings have no entry. */
29181
+ boundDataByBinding = this._boundDataByBinding.asReadonly();
29182
+ /**
29183
+ * Bindings whose load failed, for the error badge (AB#8023). Only for widgets
29184
+ * with named `bindings`: a legacy single-binding widget keeps its previous
29185
+ * behaviour (diagram without values, no badge).
29186
+ */
29187
+ bindingErrors = computed(() => {
29188
+ const errors = [];
29189
+ const results = this._boundDataByBinding();
29190
+ if (!hasNamedProcessBindings(this.config)) {
29191
+ return errors;
29192
+ }
29193
+ for (const [name, result] of results) {
29194
+ if (result.error) {
29195
+ errors.push({ name, message: result.error });
29196
+ }
29197
+ }
29198
+ return errors;
29199
+ }, /* @ts-ignore */
29200
+ ...(ngDevMode ? [{ debugName: "bindingErrors" }] : /* istanbul ignore next */ []));
29201
+ /** Text of the error badge: "Binding pv: <message>; Binding netz: …". */
29202
+ bindingErrorText = computed(() => this.bindingErrors().map(e => `Binding ${e.name}: ${e.message}`).join('; '), /* @ts-ignore */
29203
+ ...(ngDevMode ? [{ debugName: "bindingErrorText" }] : /* istanbul ignore next */ []));
26699
29204
  // Computed SVG properties
26700
29205
  viewBox = computed(() => {
26701
29206
  const diagram = this._diagramConfig();
@@ -26723,7 +29228,7 @@ class ProcessWidgetComponent {
26723
29228
  animationEnabledStates = computed(() => {
26724
29229
  const states = {};
26725
29230
  const diagram = this._diagramConfig();
26726
- const boundData = this._boundData();
29231
+ const boundData = this._boundDataByBinding();
26727
29232
  if (!diagram?.propertyBindings || diagram.propertyBindings.length === 0) {
26728
29233
  return states;
26729
29234
  }
@@ -26733,7 +29238,7 @@ class ProcessWidgetComponent {
26733
29238
  return states;
26734
29239
  }
26735
29240
  // Build property values from bound data
26736
- const propertyValues = boundData
29241
+ const propertyValues = boundData.size > 0
26737
29242
  ? this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData)
26738
29243
  : {};
26739
29244
  // Evaluate each animation binding
@@ -26761,18 +29266,24 @@ class ProcessWidgetComponent {
26761
29266
  */
26762
29267
  boundPrimitives = computed(() => {
26763
29268
  const diagram = this._diagramConfig();
26764
- const boundData = this._boundData();
29269
+ const boundData = this._boundDataByBinding();
26765
29270
  if (!diagram?.primitives) {
26766
29271
  return [];
26767
29272
  }
26768
- // If no bindings or no bound data, return original primitives
26769
- if (!diagram.propertyBindings || diagram.propertyBindings.length === 0 || !boundData) {
29273
+ // If no bindings, return original primitives. Without bound data only a
29274
+ // text.value binding changes anything: it shows "–" instead of the static
29275
+ // design-time text (AB#8022); every other effect waits for data as before.
29276
+ if (!diagram.propertyBindings || diagram.propertyBindings.length === 0) {
29277
+ return diagram.primitives;
29278
+ }
29279
+ if (boundData.size === 0 && !diagram.propertyBindings.some(b => b.effectType === 'text.value')) {
26770
29280
  return diagram.primitives;
26771
29281
  }
26772
29282
  // Build property values from bound data using mappings
26773
- const propertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData);
29283
+ const noValue = new Set();
29284
+ const propertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, noValue);
26774
29285
  // Apply bindings to primitives
26775
- return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues);
29286
+ return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues, diagram.transformProperties, noValue);
26776
29287
  }, /* @ts-ignore */
26777
29288
  ...(ngDevMode ? [{ debugName: "boundPrimitives" }] : /* istanbul ignore next */ []));
26778
29289
  ngOnInit() {
@@ -26790,9 +29301,11 @@ class ProcessWidgetComponent {
26790
29301
  * Loads the process diagram configuration and data
26791
29302
  */
26792
29303
  async loadDiagram() {
29304
+ const generation = ++this.loadGeneration;
26793
29305
  this._isLoading.set(true);
26794
29306
  this._error.set(null);
26795
29307
  this._boundData.set(null);
29308
+ this._boundDataByBinding.set(new Map());
26796
29309
  // A bound entity-selector scope that is still resolving keeps the widget loading
26797
29310
  // until the refresh that follows the resolution (AB#7721).
26798
29311
  let scopePending = false;
@@ -26802,7 +29315,7 @@ class ProcessWidgetComponent {
26802
29315
  this._diagramConfig.set(this.config.inlineConfig);
26803
29316
  await this.loadSymbolDefinitions(this.config.inlineConfig);
26804
29317
  await this.loadRuntimeData();
26805
- scopePending = await this.loadBoundData();
29318
+ scopePending = await this.loadBoundData(generation);
26806
29319
  return;
26807
29320
  }
26808
29321
  // Load from backend by rtId
@@ -26811,7 +29324,7 @@ class ProcessWidgetComponent {
26811
29324
  this._diagramConfig.set(diagram);
26812
29325
  await this.loadSymbolDefinitions(diagram);
26813
29326
  await this.loadRuntimeData();
26814
- scopePending = await this.loadBoundData();
29327
+ scopePending = await this.loadBoundData(generation);
26815
29328
  return;
26816
29329
  }
26817
29330
  // No configuration
@@ -26822,7 +29335,7 @@ class ProcessWidgetComponent {
26822
29335
  this._error.set('Failed to load process diagram');
26823
29336
  }
26824
29337
  finally {
26825
- if (!scopePending) {
29338
+ if (!scopePending && generation === this.loadGeneration) {
26826
29339
  this._isLoading.set(false);
26827
29340
  }
26828
29341
  }
@@ -26831,17 +29344,34 @@ class ProcessWidgetComponent {
26831
29344
  * Loads bound data based on widget data binding configuration.
26832
29345
  * Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
26833
29346
  */
26834
- async loadBoundData() {
26835
- // Only load if data binding is configured
26836
- if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
26837
- return false;
26838
- }
29347
+ async loadBoundData(generation) {
29348
+ this.reportBindingIssues();
26839
29349
  try {
26840
- const boundData = await this.dataService.loadBoundData(this.config);
26841
- if (boundData?.pending) {
29350
+ // Every binding loads in parallel (AB#8023): the named `bindings`, or the
29351
+ // implicit legacy binding `default`. No binding configured → nothing to load.
29352
+ const results = await this.dataService.loadBindings(this.config);
29353
+ if (generation !== this.loadGeneration) {
29354
+ // A newer load started meanwhile (e.g. leader pick changed): drop this result.
29355
+ return false;
29356
+ }
29357
+ if (results.size === 0) {
29358
+ return false;
29359
+ }
29360
+ // A pending binding (selector scope still resolving) gets no value and does
29361
+ // not blank the others. Only when every binding is pending does the widget
29362
+ // stay loading until the refresh that follows the resolution (AB#7721) —
29363
+ // for a legacy widget that is exactly its single binding.
29364
+ const loaded = new Map();
29365
+ for (const [name, result] of results) {
29366
+ if (!result.pending) {
29367
+ loaded.set(name, result);
29368
+ }
29369
+ }
29370
+ if (loaded.size === 0) {
26842
29371
  return true;
26843
29372
  }
26844
- this._boundData.set(boundData);
29373
+ this._boundDataByBinding.set(loaded);
29374
+ this._boundData.set(loaded.get(DEFAULT_PROCESS_BINDING_NAME) ?? null);
26845
29375
  }
26846
29376
  catch (err) {
26847
29377
  console.error('Error loading bound data:', err);
@@ -26849,6 +29379,22 @@ class ProcessWidgetComponent {
26849
29379
  }
26850
29380
  return false;
26851
29381
  }
29382
+ /** Writes the config issues of the bindings to the console, each once per widget instance. */
29383
+ reportBindingIssues() {
29384
+ const selectorIds = (this.stateService.getConfig().entitySelectors ?? []).map(s => s.id);
29385
+ const properties = this._diagramConfig()?.transformProperties;
29386
+ const propertyTypes = properties ? new Map(properties.map(p => [p.id, p.type])) : undefined;
29387
+ for (const issue of validateProcessWidgetBindings(this.config, selectorIds, propertyTypes)) {
29388
+ this.warnOnce(`[ProcessWidget] ${this.config.title || this.config.id}: ${issue.message}`);
29389
+ }
29390
+ }
29391
+ warnOnce(message) {
29392
+ if (this.reportedWarnings.has(message)) {
29393
+ return;
29394
+ }
29395
+ this.reportedWarnings.add(message);
29396
+ console.warn(message);
29397
+ }
26852
29398
  /**
26853
29399
  * Loads symbol definitions for all symbol instances in the diagram
26854
29400
  */
@@ -27384,11 +29930,12 @@ class ProcessWidgetComponent {
27384
29930
  return [];
27385
29931
  }
27386
29932
  // Get property values for this symbol instance
27387
- const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition);
29933
+ const noValue = new Set();
29934
+ const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition, noValue);
27388
29935
  // If there are bindings, apply them
27389
29936
  let primitives;
27390
29937
  if (definition.propertyBindings && definition.propertyBindings.length > 0) {
27391
- primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues);
29938
+ primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues, definition.transformProperties, noValue);
27392
29939
  }
27393
29940
  else {
27394
29941
  primitives = [...definition.primitives];
@@ -27396,7 +29943,7 @@ class ProcessWidgetComponent {
27396
29943
  // Add primitives from nested symbol instances (recursively)
27397
29944
  const nestedInstances = definition.symbolInstances ?? [];
27398
29945
  for (const nestedInst of nestedInstances) {
27399
- const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues);
29946
+ const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues, noValue);
27400
29947
  primitives.push(...nestedPrimitives);
27401
29948
  }
27402
29949
  return primitives;
@@ -27404,18 +29951,19 @@ class ProcessWidgetComponent {
27404
29951
  /**
27405
29952
  * Gets primitives for a nested symbol instance, with position offset applied.
27406
29953
  */
27407
- getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues) {
29954
+ getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues, parentNoValue = new Set()) {
27408
29955
  const definitions = this._symbolDefinitions();
27409
29956
  const nestedDef = definitions.get(nestedInst.symbolRtId);
27410
29957
  if (!nestedDef?.primitives) {
27411
29958
  return [];
27412
29959
  }
27413
29960
  // Calculate property values for the nested instance using "Pass to child property" bindings
27414
- const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues);
29961
+ const nestedNoValue = new Set();
29962
+ const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue, nestedNoValue);
27415
29963
  // Apply bindings to nested primitives
27416
29964
  let nestedPrimitives;
27417
29965
  if (nestedDef.propertyBindings && nestedDef.propertyBindings.length > 0) {
27418
- nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues);
29966
+ nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues, nestedDef.transformProperties, nestedNoValue);
27419
29967
  }
27420
29968
  else {
27421
29969
  nestedPrimitives = [...nestedDef.primitives];
@@ -27433,7 +29981,7 @@ class ProcessWidgetComponent {
27433
29981
  // Recursively handle nested instances within the nested symbol
27434
29982
  const deepNestedInstances = nestedDef.symbolInstances ?? [];
27435
29983
  for (const deepNestedInst of deepNestedInstances) {
27436
- const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues);
29984
+ const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues, nestedNoValue);
27437
29985
  // Apply the parent offset to deep nested primitives
27438
29986
  const deepOffsetPrimitives = deepPrimitives.map(prim => ({
27439
29987
  ...prim,
@@ -27449,7 +29997,7 @@ class ProcessWidgetComponent {
27449
29997
  /**
27450
29998
  * Calculate property values for a nested symbol instance based on "Pass to child property" bindings.
27451
29999
  */
27452
- calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues) {
30000
+ calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue = new Set(), nestedNoValue) {
27453
30001
  const nestedPropertyValues = {};
27454
30002
  // Get default values from nested symbol definition
27455
30003
  const definitions = this._symbolDefinitions();
@@ -27468,6 +30016,10 @@ class ProcessWidgetComponent {
27468
30016
  b.targetType === 'symbolInstance' &&
27469
30017
  b.targetId === nestedInst.id) ?? [];
27470
30018
  for (const binding of passThroughBindings) {
30019
+ // A parent property without a value leaves the child property without one (AB#8022)
30020
+ if (binding.targetPropertyId && parentNoValue.has(binding.propertyId)) {
30021
+ nestedNoValue?.add(binding.targetPropertyId);
30022
+ }
27471
30023
  const sourceValue = parentPropertyValues[binding.propertyId];
27472
30024
  if (sourceValue !== undefined && binding.targetPropertyId) {
27473
30025
  // Evaluate expression if present
@@ -27552,7 +30104,7 @@ class ProcessWidgetComponent {
27552
30104
  * Gets property values for a symbol instance.
27553
30105
  * Combines default values with any values passed from parent bindings.
27554
30106
  */
27555
- getSymbolInstancePropertyValues(symbolInstance, definition) {
30107
+ getSymbolInstancePropertyValues(symbolInstance, definition, noValue) {
27556
30108
  const values = {};
27557
30109
  // Initialize with default values from transform properties
27558
30110
  for (const prop of definition.transformProperties ?? []) {
@@ -27560,16 +30112,26 @@ class ProcessWidgetComponent {
27560
30112
  }
27561
30113
  // Override with values from parent diagram bindings (via 'property' effect type)
27562
30114
  const diagram = this._diagramConfig();
27563
- const boundData = this._boundData();
27564
- if (diagram?.propertyBindings && boundData) {
30115
+ const boundData = this._boundDataByBinding();
30116
+ if (diagram?.propertyBindings) {
27565
30117
  // Get parent property values
27566
- const parentPropertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData);
30118
+ const parentNoValue = new Set();
30119
+ const parentPropertyValues = this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData, parentNoValue);
27567
30120
  // Find bindings that pass values to this symbol instance
27568
30121
  const propertyBindings = diagram.propertyBindings.filter(b => b.targetType === 'symbolInstance' &&
27569
30122
  b.targetId === symbolInstance.id &&
27570
30123
  b.effectType === 'property' &&
27571
30124
  b.targetPropertyId);
27572
30125
  for (const binding of propertyBindings) {
30126
+ // A parent property without a value leaves the symbol property without one,
30127
+ // so a text.value binding inside the symbol shows "–" (AB#8022)
30128
+ if (parentNoValue.has(binding.propertyId)) {
30129
+ noValue?.add(binding.targetPropertyId);
30130
+ }
30131
+ // Without bound data the symbol keeps its defaults, as before
30132
+ if (boundData.size === 0) {
30133
+ continue;
30134
+ }
27573
30135
  const parentValue = parentPropertyValues[binding.propertyId];
27574
30136
  if (parentValue === undefined) {
27575
30137
  continue;
@@ -27990,7 +30552,7 @@ class ProcessWidgetComponent {
27990
30552
  * Maps diagram exposed properties to values from entity attributes or query columns.
27991
30553
  * Applies optional expressions for value transformation/normalization.
27992
30554
  */
27993
- buildPropertyValuesFromBoundData(transformProperties, mappings, boundData) {
30555
+ buildPropertyValuesFromBoundData(transformProperties, mappings, boundDataByBinding, noValue) {
27994
30556
  const values = {};
27995
30557
  // Initialize with default values from transform properties
27996
30558
  for (const prop of transformProperties) {
@@ -28001,14 +30563,42 @@ class ProcessWidgetComponent {
28001
30563
  const prop = transformProperties.find(p => p.id === mapping.propertyId);
28002
30564
  if (!prop)
28003
30565
  continue;
30566
+ // Each mapping reads from its own binding (AB#8023). A pending binding has
30567
+ // no entry and leaves the default; an unknown name is reported once.
30568
+ const boundData = boundDataByBinding.get(mappingBindingName(mapping));
30569
+ if (!boundData) {
30570
+ // No data yet: a text.value binding shows "–", other effects keep the default
30571
+ noValue?.add(mapping.propertyId);
30572
+ continue;
30573
+ }
28004
30574
  let rawValue = undefined;
28005
30575
  if (mapping.sourceType === 'attribute' && boundData.type === 'entity' && boundData.entity) {
28006
30576
  // Extract value from entity attribute
28007
30577
  rawValue = this.extractAttributeValue(boundData.entity, mapping.sourcePath);
28008
30578
  }
28009
30579
  else if (mapping.sourceType === 'column' && boundData.type === 'query' && boundData.queryResult) {
28010
- // Extract value from query result (first row)
28011
- rawValue = this.extractQueryColumnValue(boundData.queryResult, mapping.sourcePath);
30580
+ // First row, or the aggregate over all rows of the scope (AB#8024).
30581
+ // An aggregate on a non-number property falls back to the first row;
30582
+ // the validator has reported it once.
30583
+ const aggregate = prop.type === 'number' ? mapping.aggregate : undefined;
30584
+ rawValue = aggregateQueryColumn(boundData.queryResult, mapping.sourcePath, aggregate);
30585
+ if (isReducingAggregate(aggregate) && isPartialAggregate(boundData.queryResult)) {
30586
+ this.warnOnce(`[ProcessWidget] ${this.config.title || this.config.id}: aggregate '${aggregate}' of binding ` +
30587
+ `'${mappingBindingName(mapping)}' covers ${boundData.queryResult.rows.length} of ` +
30588
+ `${boundData.queryResult.totalCount} rows`);
30589
+ }
30590
+ if (isReducingAggregate(aggregate) && rawValue === undefined) {
30591
+ // No row / no numeric cell: the property has no value (not 0, not
30592
+ // its default), so its effects are skipped and a text shows "–".
30593
+ values[mapping.propertyId] = undefined;
30594
+ noValue?.add(mapping.propertyId);
30595
+ continue;
30596
+ }
30597
+ }
30598
+ if (rawValue === undefined || rawValue === null) {
30599
+ // Missing attribute or empty cell (AB#8022). The values map keeps its
30600
+ // legacy content (default, or null coerced) for every other effect.
30601
+ noValue?.add(mapping.propertyId);
28012
30602
  }
28013
30603
  if (rawValue !== undefined) {
28014
30604
  let finalValue = rawValue;
@@ -28032,6 +30622,11 @@ class ProcessWidgetComponent {
28032
30622
  console.warn(`[ProcessWidget] Expression error for ${mapping.propertyId}:`, err);
28033
30623
  }
28034
30624
  }
30625
+ // A number property whose value is not numeric has no value for a text
30626
+ // (coerceToType turns it into 0, which a text must never show)
30627
+ if (prop.type === 'number' && !isNumericValue(finalValue)) {
30628
+ noValue?.add(mapping.propertyId);
30629
+ }
28035
30630
  // Convert to expected type
28036
30631
  values[mapping.propertyId] = this.coerceToType(finalValue, prop.type);
28037
30632
  }
@@ -28082,17 +30677,6 @@ class ProcessWidgetComponent {
28082
30677
  }
28083
30678
  return value;
28084
30679
  }
28085
- /**
28086
- * Extracts a column value from query results (first row).
28087
- */
28088
- extractQueryColumnValue(queryResult, columnName) {
28089
- if (!queryResult.rows || queryResult.rows.length === 0)
28090
- return undefined;
28091
- const firstRow = queryResult.rows[0];
28092
- // Try both the column name and sanitized version (dots -> underscores)
28093
- const value = firstRow.cells.get(columnName) ?? firstRow.cells.get(columnName.replace(/\./g, '_'));
28094
- return value;
28095
- }
28096
30680
  /**
28097
30681
  * Coerces a value to the expected type.
28098
30682
  */
@@ -28124,7 +30708,7 @@ class ProcessWidgetComponent {
28124
30708
  * Applies property bindings to primitives.
28125
30709
  * Creates clones of primitives with binding effects applied.
28126
30710
  */
28127
- applyBindingsToPrimitives(primitives, bindings, propertyValues) {
30711
+ applyBindingsToPrimitives(primitives, bindings, propertyValues, properties, noValue) {
28128
30712
  return primitives.map(primitive => {
28129
30713
  // Find bindings for this primitive
28130
30714
  const primitiveBindings = bindings.filter(b => b.targetType === 'primitive' && b.targetId === primitive.id);
@@ -28135,6 +30719,17 @@ class ProcessWidgetComponent {
28135
30719
  let modified = this.clonePrimitive(primitive);
28136
30720
  for (const binding of primitiveBindings) {
28137
30721
  const propertyValue = propertyValues[binding.propertyId];
30722
+ if (binding.effectType === 'text.value') {
30723
+ // Formatted live value; "–" without a value, never 0 or the static text (AB#8022)
30724
+ const input = noValue?.has(binding.propertyId) ? undefined : propertyValue;
30725
+ const text = evaluateTextValue(input, binding, {
30726
+ evaluator: this.expressionEvaluator,
30727
+ locale: this.hostLanguage() ?? WIDGET_NUMBER_LOCALE,
30728
+ property: properties?.find(p => p.id === binding.propertyId)
30729
+ });
30730
+ modified = applyTextValueToPrimitive(modified, text);
30731
+ continue;
30732
+ }
28138
30733
  if (propertyValue === undefined)
28139
30734
  continue;
28140
30735
  // Build expression context
@@ -29241,8 +31836,14 @@ class ProcessWidgetComponent {
29241
31836
  <span>No process diagram configured</span>
29242
31837
  </div>
29243
31838
  }
31839
+ @if (!isLoading() && !error() && bindingErrors().length > 0) {
31840
+ <div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
31841
+ <span class="binding-error-icon" aria-hidden="true">⚠</span>
31842
+ <span class="binding-error-text">{{ bindingErrorText() }}</span>
31843
+ </div>
31844
+ }
29244
31845
  </div>
29245
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
31846
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
29246
31847
  }
29247
31848
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ProcessWidgetComponent, decorators: [{
29248
31849
  type: Component,
@@ -30242,14 +32843,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
30242
32843
  <span>No process diagram configured</span>
30243
32844
  </div>
30244
32845
  }
32846
+ @if (!isLoading() && !error() && bindingErrors().length > 0) {
32847
+ <div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
32848
+ <span class="binding-error-icon" aria-hidden="true">⚠</span>
32849
+ <span class="binding-error-text">{{ bindingErrorText() }}</span>
32850
+ </div>
32851
+ }
30245
32852
  </div>
30246
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
32853
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
30247
32854
  }], propDecorators: { config: [{
30248
32855
  type: Input
30249
32856
  }], containerRef: [{
30250
32857
  type: ViewChild,
30251
32858
  args: ['containerRef']
30252
32859
  }] } });
32860
+ /** Whether a mapped value is a finite number or a numeric string (AB#8022). */
32861
+ function isNumericValue(value) {
32862
+ if (typeof value === 'number') {
32863
+ return Number.isFinite(value);
32864
+ }
32865
+ if (typeof value === 'string' && value.trim() !== '') {
32866
+ return Number.isFinite(Number(value.trim()));
32867
+ }
32868
+ return false;
32869
+ }
30253
32870
 
30254
32871
  /**
30255
32872
  * Configuration dialog for Process Widget.
@@ -31453,6 +34070,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
31453
34070
  * - Import and call registerProcessWidget() only in routes/components that need it
31454
34071
  * - Or use provideProcessWidget() in app config for eager loading
31455
34072
  */
34073
+ /**
34074
+ * The legacy single-binding fields of one binding named `default` (AB#8023):
34075
+ * a widget with exactly that one binding is saved in the legacy shape, so no
34076
+ * existing board is migrated silently.
34077
+ */
34078
+ function legacyFieldsOf(binding) {
34079
+ return {
34080
+ dataBindingMode: binding.mode,
34081
+ bindingCkTypeId: binding.ckTypeId,
34082
+ bindingRtId: binding.rtId,
34083
+ bindingQueryRtId: binding.queryRtId,
34084
+ bindingQueryName: binding.queryName,
34085
+ bindingQueryFamily: binding.queryFamily,
34086
+ bindingIgnoreTimeFilter: binding.ignoreTimeFilter,
34087
+ bindingEntitySelectorId: binding.entitySelectorId,
34088
+ bindingFilters: binding.filters
34089
+ };
34090
+ }
34091
+ /**
34092
+ * Normalizes the binding fields before persisting: a single binding named
34093
+ * `default` collapses into the legacy fields; anything else keeps `bindings`.
34094
+ */
34095
+ function normalizeBindings(widget) {
34096
+ if (!hasNamedProcessBindings(widget)) {
34097
+ return widget;
34098
+ }
34099
+ const bindings = widget.bindings;
34100
+ if (bindings.length === 1 && bindings[0].name === DEFAULT_PROCESS_BINDING_NAME) {
34101
+ const { bindings: _dropped, ...rest } = widget;
34102
+ return { ...rest, ...legacyFieldsOf(bindings[0]) };
34103
+ }
34104
+ return widget;
34105
+ }
31456
34106
  /**
31457
34107
  * Registers the Process Widget with the widget registry.
31458
34108
  *
@@ -31508,7 +34158,19 @@ function registerProcessWidget(registry) {
31508
34158
  initialBindingFilters: widget.bindingFilters,
31509
34159
  initialPropertyMappings: widget.propertyMappings
31510
34160
  }),
31511
- applyConfigResult: (widget, result) => ({
34161
+ // A widget with named bindings (AB#8023) keeps its bindings and mappings: the
34162
+ // legacy dialog edits only the single-binding fields and would drop the
34163
+ // mappings when its own mode is 'none' (the editor for bindings is AB#8025).
34164
+ applyConfigResult: (widget, result) => hasNamedProcessBindings(widget) ? {
34165
+ ...widget,
34166
+ processDiagramRtId: result.processDiagramRtId,
34167
+ inlineConfig: result.inlineConfig,
34168
+ fitToBounds: result.fitToBounds,
34169
+ allowZoom: result.allowZoom,
34170
+ allowPan: result.allowPan,
34171
+ showToolbar: result.showToolbar,
34172
+ initialZoom: result.initialZoom
34173
+ } : ({
31512
34174
  ...widget,
31513
34175
  // Diagram selection
31514
34176
  processDiagramRtId: result.processDiagramRtId,
@@ -31548,35 +34210,42 @@ function registerProcessWidget(registry) {
31548
34210
  initialZoom: 1
31549
34211
  }),
31550
34212
  // SOLID: Serialization for persistence
31551
- toPersistedConfig: (widget) => ({
31552
- dataSourceType: 'static',
31553
- config: {
31554
- // Diagram selection
31555
- processDiagramRtId: widget.processDiagramRtId,
31556
- inlineConfig: widget.inlineConfig,
31557
- // Display options
31558
- fitToBounds: widget.fitToBounds,
31559
- allowZoom: widget.allowZoom,
31560
- allowPan: widget.allowPan,
31561
- showToolbar: widget.showToolbar,
31562
- initialZoom: widget.initialZoom,
31563
- // Data binding
31564
- dataBindingMode: widget.dataBindingMode,
31565
- bindingCkTypeId: widget.bindingCkTypeId,
31566
- bindingRtId: widget.bindingRtId,
31567
- bindingQueryRtId: widget.bindingQueryRtId,
31568
- bindingQueryName: widget.bindingQueryName,
31569
- // Stream-data binding — written only when set.
31570
- ...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
31571
- ...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
31572
- ...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
31573
- bindingFilters: widget.bindingFilters,
31574
- propertyMappings: widget.propertyMappings
31575
- }
31576
- }),
34213
+ toPersistedConfig: (input) => {
34214
+ const widget = normalizeBindings(input);
34215
+ return {
34216
+ dataSourceType: 'static',
34217
+ config: {
34218
+ // Diagram selection
34219
+ processDiagramRtId: widget.processDiagramRtId,
34220
+ inlineConfig: widget.inlineConfig,
34221
+ // Display options
34222
+ fitToBounds: widget.fitToBounds,
34223
+ allowZoom: widget.allowZoom,
34224
+ allowPan: widget.allowPan,
34225
+ showToolbar: widget.showToolbar,
34226
+ initialZoom: widget.initialZoom,
34227
+ // Data binding
34228
+ dataBindingMode: widget.dataBindingMode,
34229
+ bindingCkTypeId: widget.bindingCkTypeId,
34230
+ bindingRtId: widget.bindingRtId,
34231
+ bindingQueryRtId: widget.bindingQueryRtId,
34232
+ bindingQueryName: widget.bindingQueryName,
34233
+ // Stream-data binding — written only when set.
34234
+ ...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
34235
+ ...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
34236
+ ...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
34237
+ bindingFilters: widget.bindingFilters,
34238
+ propertyMappings: widget.propertyMappings,
34239
+ // Named bindings (AB#8023) — written only when present, so legacy
34240
+ // widgets encode byte-identically.
34241
+ ...(hasNamedProcessBindings(widget) && { bindings: toPersistedProcessBindings(widget.bindings) })
34242
+ }
34243
+ };
34244
+ },
31577
34245
  // SOLID: Deserialization from persistence
31578
34246
  fromPersistedConfig: (data, base) => {
31579
34247
  const config = parseConfig(data);
34248
+ const bindings = parsePersistedProcessBindings(config['bindings']);
31580
34249
  return {
31581
34250
  ...base,
31582
34251
  rtId: data.rtId,
@@ -31604,7 +34273,9 @@ function registerProcessWidget(registry) {
31604
34273
  bindingIgnoreTimeFilter: config['bindingIgnoreTimeFilter'],
31605
34274
  bindingEntitySelectorId: config['bindingEntitySelectorId'],
31606
34275
  bindingFilters: config['bindingFilters'],
31607
- propertyMappings: config['propertyMappings']
34276
+ propertyMappings: config['propertyMappings'],
34277
+ // Named bindings (AB#8023); absent key stays absent.
34278
+ ...(bindings && { bindings })
31608
34279
  };
31609
34280
  }
31610
34281
  });
@@ -32745,7 +35416,9 @@ function registerDefaultWidgets(registry) {
32745
35416
  initialShowDataLabels: barWidget.showDataLabels,
32746
35417
  initialColorThresholds: barWidget.colorThresholds,
32747
35418
  initialDefaultBarColor: barWidget.defaultBarColor,
32748
- initialFilters: barWidget.filters
35419
+ initialFilters: barWidget.filters,
35420
+ initialAdditionalSeries: barWidget.additionalSeries,
35421
+ initialPrimarySeries: barWidget.primarySeries
32749
35422
  };
32750
35423
  },
32751
35424
  applyConfigResult: (widget, result) => {
@@ -32776,7 +35449,9 @@ function registerDefaultWidgets(registry) {
32776
35449
  showDataLabels: result.showDataLabels,
32777
35450
  colorThresholds: result.colorThresholds,
32778
35451
  defaultBarColor: result.defaultBarColor,
32779
- filters
35452
+ filters,
35453
+ additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
35454
+ primarySeries: barPrimaryOrUndefined(result.primarySeries)
32780
35455
  };
32781
35456
  },
32782
35457
  // SOLID: Factory function
@@ -32810,6 +35485,9 @@ function registerDefaultWidgets(registry) {
32810
35485
  valueScale: widget.valueScale,
32811
35486
  colorThresholds: widget.colorThresholds,
32812
35487
  defaultBarColor: widget.defaultBarColor,
35488
+ // AB#7953: written only when set, so a single-query bar chart encodes exactly as before.
35489
+ ...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
35490
+ ...(barPrimaryOrUndefined(widget.primarySeries) && { primarySeries: barPrimaryOrUndefined(widget.primarySeries) }),
32813
35491
  queryName: widget.dataSource.queryName,
32814
35492
  queryRtId: widget.dataSource.queryRtId,
32815
35493
  ...(widget.dataSource.queryFamily && {
@@ -32845,7 +35523,8 @@ function registerDefaultWidgets(registry) {
32845
35523
  valueScale: config['valueScale'],
32846
35524
  colorThresholds: config['colorThresholds'],
32847
35525
  defaultBarColor: config['defaultBarColor'],
32848
- filters: config['filters']
35526
+ filters: config['filters'],
35527
+ ...barChartSeriesFromPersisted(config)
32849
35528
  };
32850
35529
  }
32851
35530
  });
@@ -32881,7 +35560,10 @@ function registerDefaultWidgets(registry) {
32881
35560
  initialShowMarkers: lineWidget.showMarkers,
32882
35561
  initialShowDataBadge: lineWidget.showDataBadge,
32883
35562
  initialReferenceLines: lineWidget.referenceLines,
32884
- initialFilters: lineWidget.filters
35563
+ initialFilters: lineWidget.filters,
35564
+ initialAdditionalSeries: lineWidget.additionalSeries,
35565
+ initialStacked: lineWidget.stacked,
35566
+ initialPrimarySeries: lineWidget.primarySeries
32885
35567
  };
32886
35568
  },
32887
35569
  applyConfigResult: (widget, result) => {
@@ -32917,7 +35599,10 @@ function registerDefaultWidgets(registry) {
32917
35599
  showMarkers: result.showMarkers,
32918
35600
  showDataBadge: result.showDataBadge,
32919
35601
  referenceLines: result.referenceLines,
32920
- filters
35602
+ filters,
35603
+ additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
35604
+ stacked: result.stacked || undefined,
35605
+ primarySeries: displayOptionsOrUndefined(result.primarySeries)
32921
35606
  };
32922
35607
  },
32923
35608
  createDefaultConfig: (base) => ({
@@ -32968,7 +35653,12 @@ function registerDefaultWidgets(registry) {
32968
35653
  ...(widget.dataSource.aggregateSeriesByGroup && {
32969
35654
  aggregateSeriesByGroup: true
32970
35655
  }),
32971
- filters: widget.filters
35656
+ filters: widget.filters,
35657
+ // AB#7951: written only when set, so a single-query chart encodes exactly as before.
35658
+ ...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
35659
+ ...(widget.stacked && { stacked: true }),
35660
+ // AB#7952: written only when a primary display option is not default.
35661
+ ...(displayOptionsOrUndefined(widget.primarySeries) && { primarySeries: displayOptionsOrUndefined(widget.primarySeries) })
32972
35662
  }
32973
35663
  }),
32974
35664
  fromPersistedConfig: (data, base) => {
@@ -32989,7 +35679,8 @@ function registerDefaultWidgets(registry) {
32989
35679
  showMarkers: config['showMarkers'] ?? false,
32990
35680
  showDataBadge: config['showDataBadge'] ?? true,
32991
35681
  referenceLines: config['referenceLines'],
32992
- filters: config['filters']
35682
+ filters: config['filters'],
35683
+ ...lineChartSeriesFromPersisted(config)
32993
35684
  };
32994
35685
  }
32995
35686
  });
@@ -34025,6 +36716,72 @@ function provideWidgetRegistrations(registrations) {
34025
36716
  })
34026
36717
  ]);
34027
36718
  }
36719
+ /**
36720
+ * Reads `additionalSeries` / `stacked` / `primarySeries` of a persisted line chart (AB#7951,
36721
+ * AB#7952). All keys are absent on a single-query chart, so the result is `{}` and the loaded config
36722
+ * has no such keys. Kept entries: query series with a key and a data source, formula series with a
36723
+ * key and a formula string (AB#7952). Entries of an unknown `kind` are dropped (a newer library may
36724
+ * store kinds this version does not know). The display options `visible` / `stacked` are kept only
36725
+ * as `false`.
36726
+ */
36727
+ function lineChartSeriesFromPersisted(config) {
36728
+ const out = {};
36729
+ const raw = config['additionalSeries'];
36730
+ if (Array.isArray(raw)) {
36731
+ const series = raw.filter((s) => {
36732
+ if (!s || typeof s !== 'object')
36733
+ return false;
36734
+ const entry = s;
36735
+ if (typeof entry.key !== 'string')
36736
+ return false;
36737
+ if (entry.kind === 'query')
36738
+ return !!entry.dataSource && typeof entry.dataSource.queryRtId === 'string';
36739
+ if (entry.kind === 'formula')
36740
+ return typeof entry.formula === 'string';
36741
+ return false;
36742
+ });
36743
+ if (series.length > 0)
36744
+ out.additionalSeries = series;
36745
+ }
36746
+ if (config['stacked'] === true)
36747
+ out.stacked = true;
36748
+ const primary = config['primarySeries'];
36749
+ if (primary && typeof primary === 'object') {
36750
+ const options = displayOptionsOrUndefined(primary);
36751
+ if (options)
36752
+ out.primarySeries = options;
36753
+ }
36754
+ return out;
36755
+ }
36756
+ /**
36757
+ * Reads `additionalSeries` / `primarySeries` of a persisted bar chart (AB#7953) with the line chart
36758
+ * rules (unknown kinds dropped, display options only as `false`). The bar chart keeps only
36759
+ * `visible` of the primary options. Both keys are absent on a single-query bar chart.
36760
+ */
36761
+ function barChartSeriesFromPersisted(config) {
36762
+ const line = lineChartSeriesFromPersisted({ additionalSeries: config['additionalSeries'], primarySeries: config['primarySeries'] });
36763
+ const out = {};
36764
+ if (line.additionalSeries)
36765
+ out.additionalSeries = line.additionalSeries;
36766
+ const primary = barPrimaryOrUndefined(line.primarySeries);
36767
+ if (primary)
36768
+ out.primarySeries = primary;
36769
+ return out;
36770
+ }
36771
+ /** Bar chart primary options: `{ visible: false }` or `undefined`. */
36772
+ function barPrimaryOrUndefined(options) {
36773
+ return options?.visible === false ? { visible: false } : undefined;
36774
+ }
36775
+ /** Display options reduced to their non-default (`false`) values; `undefined` when all are default. */
36776
+ function displayOptionsOrUndefined(options) {
36777
+ if (!options)
36778
+ return undefined;
36779
+ const out = {
36780
+ ...(options.visible === false && { visible: false }),
36781
+ ...(options.stacked === false && { stacked: false })
36782
+ };
36783
+ return Object.keys(out).length > 0 ? out : undefined;
36784
+ }
34028
36785
 
34029
36786
  /** True when the selector is configured to follow another selector. */
34030
36787
  function isFollowerSelector(selector) {
@@ -43229,5 +45986,5 @@ function provideCockpitWidgets() {
43229
45986
  * Generated bundle index. Do not edit.
43230
45987
  */
43231
45988
 
43232
- export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
45989
+ export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartSeriesEditorComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, alignOnTimeGrid, blueprintUpdatesKpi, bucketStepMs, buildHourlyHistogram, categoryStatus, chartSeriesColors, circularReferenceMessage, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, evaluateSeriesFormulas, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, orderSeriesFormulas, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, seriesFormulaOperands, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, validateSeriesFormulas, versionKpi, withMeshBoardNavigation };
43233
45990
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map