@meshmakers/octo-meshboard 3.5.520 → 3.5.550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
1
+ import { FieldFilterOperatorsDto, CkTypeSelectorService, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, DeleteStrategiesDto, AssociationModOptionsDto, CkModelService, TENANT_ID_PROVIDER, OCTO_SILENT_ERRORS, QueryModeDto, GraphDirectionDto, AttributeSelectorService, GraphQL, GetCkTypeAvailableQueryColumnsDtoGQL, SortOrdersDto, SeriesResolutionSignalDto, HealthService, AssetRepoService, JobManagementService, CONFIGURATION_SERVICE, SystemCommunicationDeploymentStateDto, SystemCommunicationConfigurationStateDto, HealthStatus } from '@meshmakers/octo-services';
2
2
  export { RuntimeEntityDialogDataSource, RuntimeEntitySelectDataSource, TENANT_ID_PROVIDER } from '@meshmakers/octo-services';
3
3
  import * as i0 from '@angular/core';
4
- import { Injectable, inject, EventEmitter, forwardRef, Output, Input, ViewChild, ChangeDetectionStrategy, Component, input, Directive, model, computed, Injector, EnvironmentInjector, ApplicationRef, signal, effect, untracked, InjectionToken, isSignal, LOCALE_ID, NgZone, DestroyRef, ElementRef, createEnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, provideAppInitializer, 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) {
@@ -4350,6 +4371,49 @@ class QueryExecutorService {
4350
4371
  };
4351
4372
  }));
4352
4373
  }
4374
+ /**
4375
+ * Column metadata of a persisted stream-data query for the widget config pickers.
4376
+ * Runs the query with `first: 1` and its own window first; a query that cannot run
4377
+ * without a window — an `Integral` column over a raw archive requires from/to
4378
+ * (AB#7921) — fails or returns no columns, so it is run once more with `window`
4379
+ * (the board's time window or a recent default, see `columnProbeWindow`). The error of
4380
+ * the retry propagates. `options.forceRefresh` bypasses the Apollo cache for both calls.
4381
+ *
4382
+ * The first, window-less attempt is a probe: its error is expected for such queries
4383
+ * and is not shown as an error toast (AB#7985). Only the retry's error reaches the
4384
+ * global error handler.
4385
+ */
4386
+ async fetchStreamDataColumns(queryRtId, window, options = {}) {
4387
+ const result = await this.executeWithProbeWindow('streamData', queryRtId, { first: 1, forceRefresh: options.forceRefresh ?? false }, window);
4388
+ return result.columns;
4389
+ }
4390
+ /**
4391
+ * Runs a persisted query for a config picker (column list, category values, AB#7975).
4392
+ * Runtime queries run once as {@link execute}. A stream-data query runs with its own
4393
+ * window first — silently, its refusal is expected for an Integral query without from/to
4394
+ * (AB#7985) — and, when that fails or returns no columns, once more with `window`
4395
+ * (`columnProbeWindow`). The error of the retry propagates and is shown.
4396
+ */
4397
+ async executeWithProbeWindow(family, queryRtId, options, window) {
4398
+ const resolved = family ?? await this.resolveFamily(queryRtId);
4399
+ if (resolved !== 'streamData') {
4400
+ return firstValueFrom(this.executeRuntime(queryRtId, options));
4401
+ }
4402
+ try {
4403
+ const result = await firstValueFrom(this.executeStreamData(queryRtId, { ...options, silentErrors: true }));
4404
+ if (result.columns.length > 0) {
4405
+ return result;
4406
+ }
4407
+ }
4408
+ catch {
4409
+ // Fall through to the windowed retry below.
4410
+ }
4411
+ return firstValueFrom(this.executeStreamData(queryRtId, {
4412
+ ...options,
4413
+ silentErrors: false,
4414
+ streamDataArgs: { ...options.streamDataArgs, from: window.from, to: window.to }
4415
+ }));
4416
+ }
4353
4417
  /**
4354
4418
  * Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
4355
4419
  * query for a base archive family, time window and target point count — so a widget can
@@ -6422,14 +6486,20 @@ const SUPPORTED_ROW_TYPES$1 = [
6422
6486
  function sanitizeFieldName(fieldName) {
6423
6487
  return fieldName.replace(/\./g, '_');
6424
6488
  }
6489
+ /**
6490
+ * Trailing aggregation function suffix the engine appends to a wire-form column:
6491
+ * `_count`, `_sum`, `_avg`, `_min`, `_max`, and `_integral` for the stream-data
6492
+ * Integral (energy over the query window, AB#7921).
6493
+ */
6494
+ const AGGREGATION_FUNCTION_SUFFIX = /_(?:count|sum|avg|min|max|integral)$/i;
6425
6495
  /**
6426
6496
  * Strips a trailing aggregation function suffix from a path so the base path can
6427
6497
  * be compared independently of which aggregation produced it. The engine emits
6428
- * cell paths like `meterreading_count` / `amountvalue_sum` for RT aggregation,
6429
- * grouped aggregation, and stream-data variants.
6498
+ * cell paths like `meterreading_count` / `amountvalue_sum` / `importpower_integral`
6499
+ * for RT aggregation, grouped aggregation, and stream-data variants.
6430
6500
  */
6431
6501
  function stripAggregationFunctionSuffix(path) {
6432
- return path.replace(/_(?:count|sum|avg|min|max)$/i, '');
6502
+ return path.replace(AGGREGATION_FUNCTION_SUFFIX, '');
6433
6503
  }
6434
6504
  /**
6435
6505
  * Canonical key form: drop dot and underscore separators and lowercase the rest.
@@ -6443,11 +6513,11 @@ function toCanonicalAttributeKey(s) {
6443
6513
  }
6444
6514
  /**
6445
6515
  * Detects whether a path ends with the engine's aggregation function suffix
6446
- * (`_count`, `_sum`, `_avg`, `_min`, `_max`). Used to decide whether the loose
6447
- * back-compat fallback in {@link matchesAttributePath} should fire.
6516
+ * (`_count`, `_sum`, `_avg`, `_min`, `_max`, `_integral`). Used to decide whether the
6517
+ * loose back-compat fallback in {@link matchesAttributePath} should fire.
6448
6518
  */
6449
6519
  function hasAggregationFunctionSuffix(path) {
6450
- return /_(?:count|sum|avg|min|max)$/i.test(path);
6520
+ return AGGREGATION_FUNCTION_SUFFIX.test(path);
6451
6521
  }
6452
6522
  /**
6453
6523
  * Returns true when a cell's `attributePath` refers to the same source attribute
@@ -6701,10 +6771,10 @@ function processPieChartData(rows, categoryField, valueField) {
6701
6771
  /**
6702
6772
  * Applies an optional scale factor to a query value (KPI `valueMultiplier`).
6703
6773
  * Only numeric values are scaled — strings (e.g. enum labels or error
6704
- * placeholders) pass through unchanged, as does everything when no multiplier
6705
- * is configured. Use case: cumulated energy from uniformly sampled power
6706
- * (Σ(kW samples) × sampleIntervalHours → kWh); the factor is window-independent
6707
- * because SUM is linear in the sample count.
6774
+ * placeholders) and `null` (no data) pass through unchanged, as does everything
6775
+ * when no multiplier is configured. Use case: the unit factor of an energy KPI
6776
+ * over a stream-data Integral column (value × hours): 1 for kW → kWh, 0.001 for
6777
+ * W → kWh (AB#7921).
6708
6778
  */
6709
6779
  function applyValueMultiplier(value, multiplier) {
6710
6780
  if (typeof multiplier !== 'number' || typeof value !== 'number') {
@@ -7184,8 +7254,8 @@ class KpiWidgetComponent {
7184
7254
  value = this.extractGroupedAggregationValue(result);
7185
7255
  break;
7186
7256
  }
7187
- // Optional scale factor (e.g. Σ(kW samples) × sampleInterval → kWh).
7188
- // Applied to numeric values only; strings pass through unchanged.
7257
+ // Optional unit factor (e.g. 1 for an Integral over kW → kWh, 0.001 for W → kWh).
7258
+ // Applied to numeric values only; strings and null (no data) pass through unchanged.
7189
7259
  value = applyValueMultiplier(value, this.config.valueMultiplier);
7190
7260
  // Create a synthetic entity with the value
7191
7261
  const kpiEntity = {
@@ -7254,10 +7324,12 @@ class KpiWidgetComponent {
7254
7324
  /**
7255
7325
  * Extracts a cell value, preserving strings and converting numbers.
7256
7326
  * Returns the value as-is if it's a string, or parses it as a number.
7327
+ * A null cell is "no data" (e.g. an Integral over an empty window, AB#7921) and
7328
+ * stays null, so the KPI shows its empty state instead of a misleading 0.
7257
7329
  */
7258
7330
  extractCellValue(value) {
7259
7331
  if (value === null || value === undefined)
7260
- return 0;
7332
+ return null;
7261
7333
  if (typeof value === 'number')
7262
7334
  return value;
7263
7335
  if (typeof value === 'string') {
@@ -7515,6 +7587,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
7515
7587
  type: Output
7516
7588
  }] } });
7517
7589
 
7590
+ /**
7591
+ * Time window the widget config dialogs use to list the columns of a persisted
7592
+ * stream-data query (AB#7921, AB#7975).
7593
+ *
7594
+ * A query with an `Integral` (or TimeWeightedAverage / StateDuration) column over a
7595
+ * raw archive only runs with from/to, so {@link QueryExecutorService.fetchStreamDataColumns}
7596
+ * retries with this window: the board's time window when the board has a time filter,
7597
+ * otherwise the last 24 hours. The window only serves to surface the column metadata;
7598
+ * the widget itself always runs with the board window.
7599
+ */
7600
+ const COLUMN_PROBE_FALLBACK_MS = 24 * 60 * 60 * 1000;
7601
+ function columnProbeWindow(boardWindow, now = new Date()) {
7602
+ if (boardWindow) {
7603
+ return boardWindow;
7604
+ }
7605
+ return { from: new Date(now.getTime() - COLUMN_PROBE_FALLBACK_MS), to: now };
7606
+ }
7607
+
7518
7608
  /**
7519
7609
  * Configuration dialog for KPI widgets.
7520
7610
  * Allows selecting data source, value attribute, and display options.
@@ -8082,9 +8172,11 @@ class KpiConfigDialogComponent {
8082
8172
  aggregationType: c.aggregationType ?? null
8083
8173
  }));
8084
8174
  }
8085
- // Stream-data: execute with a tiny page just to surface columns.
8086
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
8087
- return sdResult.columns.map(c => ({
8175
+ // Stream-data: execute with a tiny page just to surface columns. An Integral
8176
+ // column needs from/to (AB#7921), so the executor retries with the board's
8177
+ // window — or the last 24 hours when the board has no time filter.
8178
+ const columns = await this.queryExecutor.fetchStreamDataColumns(rtId, columnProbeWindow(this.meshBoardStateService.resolveStreamDataTimeArgs()));
8179
+ return columns.map(c => ({
8088
8180
  attributePath: c.attributePath,
8089
8181
  attributeValueType: c.attributeValueType ?? '',
8090
8182
  aggregationType: c.aggregationType ?? null
@@ -8103,7 +8195,8 @@ class KpiConfigDialogComponent {
8103
8195
  try {
8104
8196
  // family may be undefined here — the executor falls back to a lookup.
8105
8197
  const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
8106
- 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()));
8107
8200
  const values = new Set();
8108
8201
  for (const row of result.rows) {
8109
8202
  if (!KpiConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
@@ -8561,7 +8654,7 @@ class KpiConfigDialogComponent {
8561
8654
  </button>
8562
8655
  </div>
8563
8656
  </div>
8564
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
8657
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
8565
8658
  }
8566
8659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
8567
8660
  type: Component,
@@ -11831,7 +11924,8 @@ class TableConfigDialogComponent {
11831
11924
  /**
11832
11925
  * The fields a persistent query returns now (`null` when unknown or the lookup failed). Runtime
11833
11926
  * queries use the metadata-only column resolver; stream-data queries execute with `first: 1`
11834
- * (no column-introspection endpoint), 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.
11835
11929
  */
11836
11930
  loadQueryFields(rtId) {
11837
11931
  if (this.queryFieldsLoad?.rtId === rtId) {
@@ -11852,8 +11946,10 @@ class TableConfigDialogComponent {
11852
11946
  .map(c => c?.attributePath ?? '')
11853
11947
  .filter(path => path !== '');
11854
11948
  }
11855
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1, forceRefresh: true }));
11856
- 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);
11857
11953
  }
11858
11954
  catch (error) {
11859
11955
  console.error('Error loading query columns:', error);
@@ -12688,9 +12784,16 @@ class GaugeWidgetComponent {
12688
12784
  value = this.extractGroupedAggregationValue(result);
12689
12785
  break;
12690
12786
  }
12691
- // Create a synthetic entity with the value
12692
12787
  if (generation !== this.loadGeneration)
12693
12788
  return;
12789
+ // A null cell is "no data" (e.g. an Integral over an empty window, AB#7975): the gauge
12790
+ // shows its empty state ("-", pointer at the scale start), not a misleading 0.
12791
+ if (value === null) {
12792
+ this._data.set(null);
12793
+ this._isLoading.set(false);
12794
+ return;
12795
+ }
12796
+ // Create a synthetic entity with the value
12694
12797
  const gaugeEntity = {
12695
12798
  rtId: 'query-entity',
12696
12799
  ckTypeId: 'system.query',
@@ -12755,7 +12858,10 @@ class GaugeWidgetComponent {
12755
12858
  }
12756
12859
  return 0;
12757
12860
  }
12861
+ /** Parses a cell value; a null cell stays null ("no data", AB#7975), unparsable text is 0. */
12758
12862
  parseNumericValue(value) {
12863
+ if (value === null || value === undefined)
12864
+ return null;
12759
12865
  if (typeof value === 'number')
12760
12866
  return value;
12761
12867
  const parsed = parseFloat(String(value));
@@ -13619,8 +13725,10 @@ class GaugeConfigDialogComponent {
13619
13725
  aggregationType: c.aggregationType ?? null
13620
13726
  }));
13621
13727
  }
13622
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
13623
- 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 => ({
13624
13732
  attributePath: c.attributePath,
13625
13733
  attributeValueType: c.attributeValueType ?? '',
13626
13734
  aggregationType: c.aggregationType ?? null
@@ -13639,7 +13747,8 @@ class GaugeConfigDialogComponent {
13639
13747
  // family may be undefined — the executor falls back to a lookup.
13640
13748
  const family = queryFamily(this.selectedPersistentQuery?.ckTypeId) ?? this.initialQueryFamily;
13641
13749
  try {
13642
- 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()));
13643
13752
  const values = new Set();
13644
13753
  for (const row of result.rows) {
13645
13754
  if (!GaugeConfigDialogComponent.INTROSPECTION_ROW_TYPES.has(row.__typename ?? ''))
@@ -14103,7 +14212,7 @@ class GaugeConfigDialogComponent {
14103
14212
  </button>
14104
14213
  </div>
14105
14214
  </div>
14106
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14215
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;padding:16px;position:relative;flex:1;overflow-y:auto}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.checkbox-label{margin-left:8px;font-weight:400}.gauge-type-item{display:flex;flex-direction:column;gap:2px}.gauge-type-label{font-weight:500}.gauge-type-desc{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d)}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.range-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}.range-input{width:80px}.range-separator{color:var(--kendo-color-subtle, #6c757d)}.color-picker{width:40px;height:32px;padding:2px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;cursor:pointer}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.required{color:var(--kendo-color-error, #dc3545)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14107
14216
  }
14108
14217
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: GaugeConfigDialogComponent, decorators: [{
14109
14218
  type: Component,
@@ -14927,14 +15036,18 @@ class PieChartWidgetComponent {
14927
15036
  const categoryCell = findCellForField(row.cells, this.config.categoryField);
14928
15037
  const valueCell = findCellForField(row.cells, this.config.valueField);
14929
15038
  const category = categoryCell ? String(categoryCell.value ?? '') : '';
15039
+ // A null cell is "no data" (e.g. an Integral over an empty window, AB#7975):
15040
+ // the slice is left out instead of being shown as 0.
15041
+ const noData = valueCell !== undefined && (valueCell.value === null || valueCell.value === undefined);
14930
15042
  let value = 0;
14931
- if (valueCell) {
15043
+ if (valueCell && !noData) {
14932
15044
  const numValue = typeof valueCell.value === 'number' ? valueCell.value : parseFloat(String(valueCell.value));
14933
15045
  value = isNaN(numValue) ? 0 : numValue;
14934
15046
  }
14935
- return { category, value };
15047
+ return { category, value, noData };
14936
15048
  })
14937
- .filter(item => item.category !== ''); // 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 }));
14938
15051
  this._rawData.set(chartData);
14939
15052
  this._isLoading.set(false);
14940
15053
  }
@@ -15329,8 +15442,10 @@ class PieChartConfigDialogComponent {
15329
15442
  aggregationType: c.aggregationType ?? null
15330
15443
  }));
15331
15444
  }
15332
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
15333
- 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 => ({
15334
15449
  attributePath: c.attributePath,
15335
15450
  attributeValueType: c.attributeValueType ?? '',
15336
15451
  aggregationType: c.aggregationType ?? null
@@ -15970,6 +16085,541 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
15970
16085
  type: Input
15971
16086
  }] } });
15972
16087
 
16088
+ /** Referenced series keys of a formula, distinct, in order of first appearance. */
16089
+ function seriesFormulaOperands(formula) {
16090
+ return rewriteFormula(formula ?? '').referencedNames;
16091
+ }
16092
+ /**
16093
+ * Orders formula series so every series comes after the formula series it references (Kahn's
16094
+ * algorithm, stable in input order). References to non-formula keys (query series) are leaves.
16095
+ * Series in or behind a cycle are left out of `order`; `cyclic` names the ones in the cycle.
16096
+ */
16097
+ function orderSeriesFormulas(formulas) {
16098
+ const keys = new Set(formulas.map(f => f.key));
16099
+ const deps = new Map();
16100
+ for (const f of formulas) {
16101
+ deps.set(f.key, seriesFormulaOperands(f.formula).filter(k => keys.has(k)));
16102
+ }
16103
+ const order = [];
16104
+ const done = new Set();
16105
+ let progress = true;
16106
+ while (progress) {
16107
+ progress = false;
16108
+ for (const f of formulas) {
16109
+ if (done.has(f.key))
16110
+ continue;
16111
+ if ((deps.get(f.key) ?? []).every(d => done.has(d))) {
16112
+ done.add(f.key);
16113
+ order.push(f.key);
16114
+ progress = true;
16115
+ }
16116
+ }
16117
+ }
16118
+ const cyclic = formulas
16119
+ .filter(f => !done.has(f.key) && reaches(f.key, f.key, deps))
16120
+ .map(f => f.key);
16121
+ return { order, cyclic };
16122
+ }
16123
+ /** True when `target` is reachable from the dependencies of `start`. */
16124
+ function reaches(start, target, deps) {
16125
+ const visited = new Set();
16126
+ const stack = [...(deps.get(start) ?? [])];
16127
+ while (stack.length > 0) {
16128
+ const current = stack.pop();
16129
+ if (current === target)
16130
+ return true;
16131
+ if (visited.has(current))
16132
+ continue;
16133
+ visited.add(current);
16134
+ stack.push(...(deps.get(current) ?? []));
16135
+ }
16136
+ return false;
16137
+ }
16138
+ /** Message of a circular reference, naming the series involved. */
16139
+ function circularReferenceMessage(cyclic) {
16140
+ return `Circular reference between series: ${cyclic.join(', ')}`;
16141
+ }
16142
+ /**
16143
+ * Validates every formula series of a chart: syntax and unknown keys exactly like the KPI formula
16144
+ * field (`validateFormula`, series keys as available names), then circular references.
16145
+ * Returns one result per formula, in input order.
16146
+ */
16147
+ function validateSeriesFormulas(formulas, availableKeys, evaluator) {
16148
+ const results = formulas.map(f => validateFormula(f.formula, availableKeys, evaluator));
16149
+ const { cyclic } = orderSeriesFormulas(formulas);
16150
+ if (cyclic.length > 0) {
16151
+ const message = circularReferenceMessage(cyclic);
16152
+ formulas.forEach((f, i) => {
16153
+ if (results[i].valid && cyclic.includes(f.key)) {
16154
+ results[i] = { valid: false, error: message, unknownVariables: [] };
16155
+ }
16156
+ });
16157
+ }
16158
+ return results;
16159
+ }
16160
+ /**
16161
+ * Evaluates the formula series bucket by bucket over `operands` (key → values, all of `length`).
16162
+ * Formula series are evaluated in dependency order and their results become operands of later
16163
+ * ones. A formula in a cycle, or with an operand key that has no values, is all gaps.
16164
+ * Returns key → values for every formula series.
16165
+ */
16166
+ function evaluateSeriesFormulas(formulas, operands, length, evaluator) {
16167
+ const values = new Map(operands);
16168
+ const result = new Map();
16169
+ const byKey = new Map(formulas.map(f => [f.key, f]));
16170
+ const { order } = orderSeriesFormulas(formulas);
16171
+ for (const key of order) {
16172
+ const formula = byKey.get(key);
16173
+ const data = evaluateOneSeries(formula.formula, values, length, evaluator);
16174
+ result.set(key, data);
16175
+ values.set(key, data);
16176
+ }
16177
+ for (const f of formulas) {
16178
+ if (!result.has(f.key))
16179
+ result.set(f.key, new Array(length).fill(null));
16180
+ }
16181
+ return result;
16182
+ }
16183
+ function evaluateOneSeries(formula, values, length, evaluator) {
16184
+ const out = new Array(length).fill(null);
16185
+ if (!formula?.trim())
16186
+ return out;
16187
+ const { expression, referencedNames } = rewriteFormula(formula);
16188
+ const columns = referencedNames.map(name => values.get(name));
16189
+ if (columns.some(c => c === undefined))
16190
+ return out;
16191
+ // Identifiers of the operands, as rewriteFormula produced them (`${a}` alone rewrites to the identifier of `a`).
16192
+ const identifiers = referencedNames.map(name => rewriteFormula(`\${${name}}`).expression);
16193
+ for (let i = 0; i < length; i++) {
16194
+ const context = {};
16195
+ let gap = false;
16196
+ for (let j = 0; j < columns.length; j++) {
16197
+ const v = columns[j][i];
16198
+ if (v === null || v === undefined || !Number.isFinite(v)) {
16199
+ gap = true;
16200
+ break;
16201
+ }
16202
+ context[identifiers[j]] = v;
16203
+ }
16204
+ if (gap)
16205
+ continue;
16206
+ const r = evaluator.evaluate(expression, context);
16207
+ if (r.success && typeof r.value === 'number' && Number.isFinite(r.value)) {
16208
+ out[i] = r.value;
16209
+ }
16210
+ }
16211
+ return out;
16212
+ }
16213
+
16214
+ /** Suffix of the legend name of a formula series without any valid bucket. */
16215
+ const NO_DATA_LEGEND_SUFFIX = ' (no data)';
16216
+ /** Source key of the primary data source; series keys cannot clash with it (they start with a letter or `_`, never `__primary`). */
16217
+ const PRIMARY_SOURCE_KEY = '__primary';
16218
+ /** Valid series key: letters, digits and `_`, starting with a letter or `_`. */
16219
+ const SERIES_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
16220
+ /** A query series the widget can load: key, query and value field set. */
16221
+ function isCompleteQuerySeries(def) {
16222
+ return def.kind === 'query'
16223
+ && SERIES_KEY_PATTERN.test(def.key ?? '')
16224
+ && def.key !== PRIMARY_SOURCE_KEY
16225
+ && !!def.dataSource?.queryRtId
16226
+ && !!def.valueField;
16227
+ }
16228
+ /** A formula series the widget can evaluate: valid key and a non-empty formula (validity is checked by the dialog). */
16229
+ function isCompleteFormulaSeries(def) {
16230
+ return def.kind === 'formula'
16231
+ && SERIES_KEY_PATTERN.test(def.key ?? '')
16232
+ && def.key !== PRIMARY_SOURCE_KEY
16233
+ && !!def.formula?.trim();
16234
+ }
16235
+ /** Display name of a series: its label, else its key. */
16236
+ function querySeriesLabel(def) {
16237
+ return def.label?.trim() || def.key;
16238
+ }
16239
+ function toNumberOrNull(value) {
16240
+ if (value === null || value === undefined)
16241
+ return null;
16242
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
16243
+ return isNaN(n) ? null : n;
16244
+ }
16245
+ /** Bucket start instant of a row: the downsampling bin / stream-data row timestamp, else the given category cell. */
16246
+ function rowInstant(row, categoryField, useTimestamp) {
16247
+ let raw = useTimestamp ? row.timestamp : undefined;
16248
+ if ((raw === undefined || raw === null) && categoryField) {
16249
+ raw = findCellForField(row.cells, categoryField)?.value;
16250
+ }
16251
+ const instant = toInstant$1(raw);
16252
+ return instant ? instant.getTime() : null;
16253
+ }
16254
+ /**
16255
+ * Value of a row for `valueField`. Downsampled rows carry `<field>_avg` / `_min` / `_max`: the
16256
+ * average (or an unsuffixed cell) is the value. Raw rows: the exact cell first (AB#4293 rule).
16257
+ * A null cell is "no data" (null), never 0.
16258
+ */
16259
+ function rowValue(row, valueField, downsampled) {
16260
+ if (downsampled) {
16261
+ const cell = row.cells.find(c => {
16262
+ const lower = c.attributePath.toLowerCase();
16263
+ return lower !== 'timestamp' && !lower.endsWith('_min') && !lower.endsWith('_max')
16264
+ && matchesAttributePath(c.attributePath, valueField);
16265
+ });
16266
+ return toNumberOrNull(cell?.value);
16267
+ }
16268
+ return toNumberOrNull(findCellForField(row.cells, valueField)?.value);
16269
+ }
16270
+ /**
16271
+ * Rows of the primary source → one series per `seriesGroupField` value, exactly the grouping of
16272
+ * the single-query chart (a later row of the same series and bucket wins).
16273
+ */
16274
+ function primaryRowsToSeries(rows, fields, downsampled) {
16275
+ const series = new Map();
16276
+ for (const row of rows) {
16277
+ const instant = rowInstant(row, downsampled ? undefined : fields.categoryField, downsampled);
16278
+ if (instant === null)
16279
+ continue;
16280
+ const group = findCellForField(row.cells, fields.seriesGroupField)?.value;
16281
+ if (group === null || group === undefined || String(group) === '')
16282
+ continue;
16283
+ const name = String(group);
16284
+ let entry = series.get(name);
16285
+ if (!entry) {
16286
+ entry = { points: new Map() };
16287
+ series.set(name, entry);
16288
+ }
16289
+ entry.points.set(instant, rowValue(row, fields.valueField, downsampled));
16290
+ if (fields.unitField) {
16291
+ const unit = findCellForField(row.cells, fields.unitField)?.value;
16292
+ if (unit !== null && unit !== undefined && String(unit) !== '')
16293
+ entry.unit = String(unit);
16294
+ }
16295
+ }
16296
+ return Array.from(series.entries()).map(([name, e]) => ({ name, points: e.points, unit: e.unit }));
16297
+ }
16298
+ /**
16299
+ * Rows of a query series → its one series. Several rows in one bucket (e.g. one per source entity
16300
+ * when the scope holds several) are summed; a bucket whose rows are all null stays null.
16301
+ */
16302
+ function queryRowsToSeries(rows, name, valueField, downsampled) {
16303
+ const points = new Map();
16304
+ for (const row of rows) {
16305
+ const instant = rowInstant(row, 'timestamp', true);
16306
+ if (instant === null)
16307
+ continue;
16308
+ const value = rowValue(row, valueField, downsampled);
16309
+ const prev = points.get(instant);
16310
+ if (prev === undefined || prev === null)
16311
+ points.set(instant, value);
16312
+ else if (value !== null)
16313
+ points.set(instant, prev + value);
16314
+ }
16315
+ return { name, points };
16316
+ }
16317
+ /**
16318
+ * Category labels for aligned instants. A label that repeats (the doubled wall-clock hour of a
16319
+ * DST fall-back day) gets ` (2)`, ` (3)` …, so every bucket keeps its own category.
16320
+ */
16321
+ function uniqueCategoryLabels(instants, format) {
16322
+ const seen = new Map();
16323
+ return instants.map(t => {
16324
+ const label = format(new Date(t));
16325
+ const n = (seen.get(label) ?? 0) + 1;
16326
+ seen.set(label, n);
16327
+ return n === 1 ? label : `${label} (${n})`;
16328
+ });
16329
+ }
16330
+ /**
16331
+ * Validation message of each series row of the config dialog (query and formula rows, AB#7951 /
16332
+ * AB#7952), `null` when the row is valid. Keys must match {@link SERIES_KEY_PATTERN} and be unique
16333
+ * across all rows; query rows need a query and a value field; formula rows are validated like the
16334
+ * KPI formula field (syntax, unknown keys — every series key is an available name) and must not be
16335
+ * part of a circular reference. With `requireCategoryField` (bar chart, AB#7953) query rows also
16336
+ * need a category field.
16337
+ */
16338
+ function querySeriesErrors(series, evaluator, options) {
16339
+ const counts = new Map();
16340
+ for (const s of series) {
16341
+ const key = s.key?.trim() ?? '';
16342
+ if (key)
16343
+ counts.set(key, (counts.get(key) ?? 0) + 1);
16344
+ }
16345
+ const keys = [...counts.keys()];
16346
+ const formulaRows = series
16347
+ .map((s, index) => ({ s, index }))
16348
+ .filter((r) => r.s.kind === 'formula');
16349
+ const formulaResults = evaluator
16350
+ ? validateSeriesFormulas(formulaRows.map(r => ({ key: r.s.key?.trim() ?? '', formula: r.s.formula ?? '' })), keys, evaluator)
16351
+ : [];
16352
+ const formulaErrorByIndex = new Map();
16353
+ formulaRows.forEach((r, i) => {
16354
+ const result = formulaResults[i];
16355
+ formulaErrorByIndex.set(r.index, result && !result.valid ? (result.error ?? 'Invalid formula') : null);
16356
+ });
16357
+ return series.map((s, index) => {
16358
+ const key = s.key?.trim() ?? '';
16359
+ if (!key)
16360
+ return 'Enter a key.';
16361
+ if (!SERIES_KEY_PATTERN.test(key) || key === PRIMARY_SOURCE_KEY) {
16362
+ return 'The key may contain letters, digits and _ and must not start with a digit.';
16363
+ }
16364
+ if ((counts.get(key) ?? 0) > 1)
16365
+ return `The key "${key}" is used by another series.`;
16366
+ if (s.kind === 'formula') {
16367
+ if (!s.formula?.trim())
16368
+ return 'Enter a formula.';
16369
+ return formulaErrorByIndex.get(index) ?? null;
16370
+ }
16371
+ if (!s.dataSource?.queryRtId)
16372
+ return 'Select a stream-data query.';
16373
+ if (options?.requireCategoryField && !s.categoryField)
16374
+ return 'Select a category field.';
16375
+ if (!s.valueField)
16376
+ return 'Select a value field.';
16377
+ if (s.dataSource.resolutionAware && !s.dataSource.requiredAggregation)
16378
+ return 'Select the aggregation.';
16379
+ return null;
16380
+ });
16381
+ }
16382
+ /**
16383
+ * Composes the series the chart draws from the aligned grid (AB#7952):
16384
+ *
16385
+ * 1. Operands = the aligned query series by key (hidden ones included — they went through the
16386
+ * alignment like every other series).
16387
+ * 2. Formula series are evaluated per bucket (`evaluateSeriesFormulas`: dependency order, null
16388
+ * operand or non-finite result = gap).
16389
+ * 3. Order: primary series, then `additionalSeries` in their order; hidden series (`visible:
16390
+ * false`, primary via `primary.visible`) are left out. In a stacked area chart a series with
16391
+ * `stacked: false` is drawn as a line and moved behind the stacked ones, so it lies over the stack.
16392
+ * 4. Hints: a visible formula series that (directly or through other formulas) uses a failed or
16393
+ * misaligned query source gets "depends on <source>, which failed"; a visible formula series without any
16394
+ * valid bucket keeps its legend entry with the suffix " (no data)".
16395
+ */
16396
+ function composeChartSeries(args) {
16397
+ const { gridSeries, length, definitions, primary, stackedChart, sourceProblems, evaluator } = args;
16398
+ const formulas = definitions.filter(isCompleteFormulaSeries);
16399
+ const operands = new Map();
16400
+ for (const s of gridSeries) {
16401
+ if (s.sourceKey !== PRIMARY_SOURCE_KEY)
16402
+ operands.set(s.sourceKey, s.data);
16403
+ }
16404
+ const evaluated = formulas.length > 0
16405
+ ? evaluateSeriesFormulas(formulas, operands, length, evaluator)
16406
+ : new Map();
16407
+ const placement = (options) => {
16408
+ const stacked = stackedChart && options?.stacked !== false;
16409
+ return { stacked, asLine: stackedChart && !stacked };
16410
+ };
16411
+ const series = [];
16412
+ if (primary?.visible !== false) {
16413
+ for (const s of gridSeries) {
16414
+ if (s.sourceKey === PRIMARY_SOURCE_KEY)
16415
+ series.push({ name: s.name, data: s.data, unit: s.unit, ...placement(primary) });
16416
+ }
16417
+ }
16418
+ const hints = [];
16419
+ const formulaByKey = new Map(formulas.map(f => [f.key, f]));
16420
+ for (const def of definitions) {
16421
+ if (def.kind === 'query') {
16422
+ if (def.visible === false || !isCompleteQuerySeries(def))
16423
+ continue;
16424
+ const aligned = gridSeries.find(s => s.sourceKey === def.key);
16425
+ if (aligned)
16426
+ series.push({ name: aligned.name, data: aligned.data, unit: aligned.unit, ...placement(def) });
16427
+ continue;
16428
+ }
16429
+ if (!isCompleteFormulaSeries(def) || def.visible === false)
16430
+ continue;
16431
+ const label = querySeriesLabel(def);
16432
+ const failed = failedSourcesOf(def.key, formulaByKey, sourceProblems);
16433
+ for (const source of failed) {
16434
+ hints.push({
16435
+ kind: 'dependency',
16436
+ sourceLabel: label,
16437
+ text: `${label}: depends on ${source.label}, which ${source.problem === 'misaligned' ? 'is not on the chart\'s time grid' : 'failed'}`
16438
+ });
16439
+ }
16440
+ const data = evaluated.get(def.key) ?? new Array(length).fill(null);
16441
+ const hasData = data.some(v => v !== null);
16442
+ series.push({ name: hasData ? label : `${label}${NO_DATA_LEGEND_SUFFIX}`, data, ...placement(def) });
16443
+ }
16444
+ // Lines over the stack come last, so Kendo draws them on top.
16445
+ return { series: [...series.filter(s => !s.asLine), ...series.filter(s => s.asLine)], hints };
16446
+ }
16447
+ /** Failed or misaligned query sources a formula series depends on, directly or through other formulas. */
16448
+ function failedSourcesOf(key, formulaByKey, sourceProblems) {
16449
+ const out = [];
16450
+ const visited = new Set();
16451
+ const stack = [key];
16452
+ while (stack.length > 0) {
16453
+ const current = stack.pop();
16454
+ if (visited.has(current))
16455
+ continue;
16456
+ visited.add(current);
16457
+ const formula = formulaByKey.get(current);
16458
+ if (formula) {
16459
+ stack.push(...seriesFormulaOperands(formula.formula).reverse());
16460
+ continue;
16461
+ }
16462
+ const problem = sourceProblems.get(current);
16463
+ if (problem && problem.problem !== 'noData')
16464
+ out.push(problem);
16465
+ }
16466
+ return out;
16467
+ }
16468
+
16469
+ /** Category of a raw cell value; `null` for an empty value (the row is skipped, as today). */
16470
+ function categoryOf(raw) {
16471
+ if (raw === null || raw === undefined || String(raw) === '')
16472
+ return null;
16473
+ if (raw instanceof Date || isIsoDateTime(raw)) {
16474
+ const instant = toInstant$1(raw);
16475
+ if (instant)
16476
+ return { key: `t:${instant.getTime()}`, instant: instant.getTime(), raw };
16477
+ }
16478
+ return { key: `s:${String(raw)}`, instant: null, raw };
16479
+ }
16480
+ /**
16481
+ * Raw category value of a row: the cell of `categoryField` (exact match first, AB#4293). A
16482
+ * stream-data row carries its time stamp as row property, not as cell, so `timestamp` falls back
16483
+ * to `row.timestamp`.
16484
+ */
16485
+ function rowCategory(row, categoryField) {
16486
+ let raw = findCellForField(row.cells, categoryField)?.value;
16487
+ if ((raw === undefined || raw === null) && categoryField.toLowerCase() === 'timestamp')
16488
+ raw = row.timestamp;
16489
+ return categoryOf(raw);
16490
+ }
16491
+ /**
16492
+ * Bar value of a cell, as the single-query chart reads it: a number, `null` for a null cell ("no
16493
+ * data", AB#7975) and 0 for text that is not a number.
16494
+ */
16495
+ function toBarValue(value) {
16496
+ if (value === null || value === undefined)
16497
+ return null;
16498
+ const n = typeof value === 'number' ? value : parseFloat(String(value));
16499
+ return isNaN(n) ? 0 : n;
16500
+ }
16501
+ function scaled(value, valueScale) {
16502
+ return value === null ? null : value * valueScale;
16503
+ }
16504
+ function addCategory(list, seen, category) {
16505
+ if (seen.has(category.key))
16506
+ return;
16507
+ seen.add(category.key);
16508
+ list.push(category);
16509
+ }
16510
+ /**
16511
+ * Rows of the primary source → its series, exactly the grouping of the single-query chart:
16512
+ * static mode one series per configured value field (a missing cell is 0), dynamic mode one series
16513
+ * per `seriesGroupField` value (a combination missing in a category of this source is 0). A later
16514
+ * row of the same category wins. `valueScale` is applied to every value.
16515
+ */
16516
+ function primaryRowsToCategorySeries(rows, config, valueScale) {
16517
+ const categories = [];
16518
+ const seen = new Set();
16519
+ if (config.seriesGroupField && config.valueField) {
16520
+ const groups = new Map();
16521
+ for (const row of rows) {
16522
+ const category = rowCategory(row, config.categoryField);
16523
+ const groupCell = findCellForField(row.cells, config.seriesGroupField);
16524
+ const group = groupCell ? String(groupCell.value ?? '') : '';
16525
+ if (!category || !group)
16526
+ continue;
16527
+ addCategory(categories, seen, category);
16528
+ const valueCell = findCellForField(row.cells, config.valueField);
16529
+ const value = valueCell ? toBarValue(valueCell.value) : 0;
16530
+ let values = groups.get(group);
16531
+ if (!values) {
16532
+ values = new Map();
16533
+ groups.set(group, values);
16534
+ }
16535
+ values.set(category.key, scaled(value, valueScale));
16536
+ }
16537
+ const series = Array.from(groups.entries()).map(([name, values]) => {
16538
+ for (const c of categories)
16539
+ if (!values.has(c.key))
16540
+ values.set(c.key, 0);
16541
+ return { name, values };
16542
+ });
16543
+ return { categories, series };
16544
+ }
16545
+ const fields = config.series ?? [];
16546
+ const series = fields.map(f => ({ name: f.name ?? f.field, values: new Map() }));
16547
+ for (const row of rows) {
16548
+ const category = rowCategory(row, config.categoryField);
16549
+ if (!category)
16550
+ continue;
16551
+ addCategory(categories, seen, category);
16552
+ fields.forEach((f, i) => {
16553
+ const cell = findCellForField(row.cells, f.field);
16554
+ series[i].values.set(category.key, cell ? scaled(toBarValue(cell.value), valueScale) : 0);
16555
+ });
16556
+ }
16557
+ return { categories, series };
16558
+ }
16559
+ /**
16560
+ * Rows of a query series → its one series. Several rows in one category (e.g. one per source
16561
+ * entity when the scope holds several) are summed; a category whose rows are all null stays null.
16562
+ * `valueScale` is applied to the result.
16563
+ */
16564
+ function queryRowsToCategorySeries(rows, name, categoryField, valueField, valueScale) {
16565
+ const categories = [];
16566
+ const seen = new Set();
16567
+ const values = new Map();
16568
+ for (const row of rows) {
16569
+ const category = rowCategory(row, categoryField);
16570
+ if (!category)
16571
+ continue;
16572
+ addCategory(categories, seen, category);
16573
+ const cell = findCellForField(row.cells, valueField);
16574
+ const value = cell ? toBarValue(cell.value) : null;
16575
+ const prev = values.get(category.key);
16576
+ if (prev === undefined || prev === null)
16577
+ values.set(category.key, value);
16578
+ else if (value !== null)
16579
+ values.set(category.key, prev + value);
16580
+ }
16581
+ for (const [key, v] of values)
16582
+ values.set(key, scaled(v, valueScale));
16583
+ return { categories, series: { name, values } };
16584
+ }
16585
+ /**
16586
+ * Matches the series of all sources per category value. Categories are the union over all sources:
16587
+ * in order of first appearance (primary source first, then the others in their order), and sorted
16588
+ * chronologically when every category is a date-time. A category a source lacks is `null` for
16589
+ * its series — no bar, never 0.
16590
+ */
16591
+ function alignOnCategories(sources) {
16592
+ const categories = [];
16593
+ const seen = new Set();
16594
+ for (const source of sources) {
16595
+ for (const c of source.categories)
16596
+ addCategory(categories, seen, c);
16597
+ }
16598
+ if (categories.length > 0 && categories.every(c => c.instant !== null)) {
16599
+ categories.sort((a, b) => a.instant - b.instant);
16600
+ }
16601
+ const series = [];
16602
+ for (const source of sources) {
16603
+ for (const s of source.series) {
16604
+ series.push({
16605
+ sourceKey: source.sourceKey,
16606
+ name: s.name,
16607
+ data: categories.map(c => (s.values.has(c.key) ? s.values.get(c.key) : null))
16608
+ });
16609
+ }
16610
+ }
16611
+ return { categories, series };
16612
+ }
16613
+ /** Makes labels unique: a repeated label gets ` (2)`, ` (3)` …, so every category keeps its own slot. */
16614
+ function uniqueLabels(labels) {
16615
+ const seen = new Map();
16616
+ return labels.map(label => {
16617
+ const n = (seen.get(label) ?? 0) + 1;
16618
+ seen.set(label, n);
16619
+ return n === 1 ? label : `${label} (${n})`;
16620
+ });
16621
+ }
16622
+
15973
16623
  const CHART_TYPE_MAPPING = {
15974
16624
  column: { type: 'column', stack: false },
15975
16625
  bar: { type: 'bar', stack: false },
@@ -15990,6 +16640,7 @@ class BarChartWidgetComponent {
15990
16640
  ]);
15991
16641
  stateService = inject(MeshBoardStateService);
15992
16642
  variableService = inject(MeshBoardVariableService);
16643
+ expressionEvaluator = inject(ExpressionEvaluatorService);
15993
16644
  config;
15994
16645
  // Widget state signals
15995
16646
  _isLoading = signal(false, /* @ts-ignore */
@@ -16004,6 +16655,13 @@ class BarChartWidgetComponent {
16004
16655
  categories = this._categories.asReadonly();
16005
16656
  seriesData = this._seriesData.asReadonly();
16006
16657
  error = this._error.asReadonly();
16658
+ // Multi-source path (AB#7953): per-source hints and whether the last load went through it.
16659
+ _sourceHints = signal([], /* @ts-ignore */
16660
+ ...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
16661
+ sourceHints = this._sourceHints.asReadonly();
16662
+ _multiSource = signal(false, /* @ts-ignore */
16663
+ ...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
16664
+ isMultiSource = this._multiSource.asReadonly();
16007
16665
  data = computed(() => this._seriesData(), /* @ts-ignore */
16008
16666
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
16009
16667
  /**
@@ -16089,6 +16747,27 @@ class BarChartWidgetComponent {
16089
16747
  isDynamicSeriesMode() {
16090
16748
  return !!(this.config.seriesGroupField && this.config.valueField);
16091
16749
  }
16750
+ /** Whether the chart reads series from more than its primary query or has formula series (AB#7953). */
16751
+ hasAdditionalSeries() {
16752
+ return (this.config?.additionalSeries ?? []).some(s => this.isLoadableQuerySeries(s) || isCompleteFormulaSeries(s));
16753
+ }
16754
+ isLoadableQuerySeries(s) {
16755
+ return isCompleteQuerySeries(s) && !!s.categoryField;
16756
+ }
16757
+ /**
16758
+ * Shared tooltip rows of the multi-source path (AB#7953): one row per drawn series for the
16759
+ * hovered category; no bar (null) shows "–".
16760
+ */
16761
+ tooltipRows(category) {
16762
+ const index = this._categories().indexOf(category);
16763
+ if (index < 0)
16764
+ return [];
16765
+ return this._seriesData().map(s => {
16766
+ const raw = s.data[index];
16767
+ const value = raw !== null && typeof raw === 'object' ? raw.value : raw;
16768
+ return { name: s.name, text: value === null || value === undefined ? '–' : this.formatValue(value) };
16769
+ });
16770
+ }
16092
16771
  formatValue(value) {
16093
16772
  return value.toLocaleString('de-AT', {
16094
16773
  minimumFractionDigits: 0,
@@ -16100,6 +16779,12 @@ class BarChartWidgetComponent {
16100
16779
  if (this.isNotConfigured()) {
16101
16780
  return;
16102
16781
  }
16782
+ if (this.hasAdditionalSeries()) {
16783
+ await this.loadMultiSource();
16784
+ return;
16785
+ }
16786
+ this._multiSource.set(false);
16787
+ this._sourceHints.set([]);
16103
16788
  // Note: isNotConfigured() check ensures dataSource and queryRtId are set
16104
16789
  const queryDataSource = this.config.dataSource;
16105
16790
  this._isLoading.set(true);
@@ -16143,6 +16828,141 @@ class BarChartWidgetComponent {
16143
16828
  this._isLoading.set(false);
16144
16829
  }
16145
16830
  }
16831
+ /**
16832
+ * Multi-source load (AB#7953): the primary query plus every complete query series, each with its
16833
+ * own scope and time-filter opt-out, matched per category value.
16834
+ *
16835
+ * 1. Scope per source: any `pending` → stay loading, no query; `empty` → no series for that
16836
+ * source and a "No data for <source>" hint.
16837
+ * 2. All sources load in parallel; a failing source gives an error hint, the others render.
16838
+ * 3. `valueScale` is applied to every source value, then {@link alignOnCategories} builds the
16839
+ * union of categories (chronological when all are date-times); a category a source lacks is
16840
+ * no bar for its series.
16841
+ * 4. `composeChartSeries` (shared with the line chart) evaluates formula series per category,
16842
+ * drops hidden series and adds "depends on <source>, which failed" hints.
16843
+ */
16844
+ async loadMultiSource() {
16845
+ const primaryDs = this.config.dataSource;
16846
+ const sources = [
16847
+ { key: PRIMARY_SOURCE_KEY, label: primaryDs.queryName?.trim() || 'Primary query', dataSource: primaryDs, primary: true },
16848
+ ...(this.config.additionalSeries ?? [])
16849
+ .filter((s) => this.isLoadableQuerySeries(s))
16850
+ .map(def => ({
16851
+ key: def.key,
16852
+ label: querySeriesLabel(def),
16853
+ dataSource: def.dataSource,
16854
+ primary: false,
16855
+ categoryField: def.categoryField,
16856
+ valueField: def.valueField
16857
+ }))
16858
+ ];
16859
+ this._isLoading.set(true);
16860
+ this._error.set(null);
16861
+ this._multiSource.set(true);
16862
+ const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
16863
+ if (scopes.some(s => s.kind === 'pending')) {
16864
+ return;
16865
+ }
16866
+ try {
16867
+ const hints = [];
16868
+ const problems = new Map();
16869
+ const noData = (source) => {
16870
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
16871
+ problems.set(source.key, { label: source.label, problem: 'noData' });
16872
+ };
16873
+ const active = [];
16874
+ sources.forEach((source, i) => {
16875
+ const scope = scopes[i];
16876
+ if (scope.kind === 'empty')
16877
+ noData(source);
16878
+ else
16879
+ active.push({ source, rtIds: scope.kind === 'scoped' ? scope.rtIds : undefined });
16880
+ });
16881
+ const fetched = await Promise.allSettled(active.map(a => this.fetchSourceRows(a.source, a.rtIds)));
16882
+ const valueScale = this.config.valueScale ?? 1;
16883
+ const categorySources = [];
16884
+ fetched.forEach((f, i) => {
16885
+ const source = active[i].source;
16886
+ if (f.status === 'rejected') {
16887
+ console.error(`Error loading Bar Chart source "${source.label}":`, f.reason);
16888
+ hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
16889
+ problems.set(source.key, { label: source.label, problem: 'error' });
16890
+ return;
16891
+ }
16892
+ const converted = source.primary
16893
+ ? primaryRowsToCategorySeries(f.value, this.config, valueScale)
16894
+ : (r => ({ categories: r.categories, series: [r.series] }))(queryRowsToCategorySeries(f.value, source.label, source.categoryField, source.valueField, valueScale));
16895
+ if (converted.categories.length === 0 || converted.series.length === 0) {
16896
+ noData(source);
16897
+ return;
16898
+ }
16899
+ categorySources.push({ sourceKey: source.key, sourceLabel: source.label, ...converted });
16900
+ });
16901
+ const aligned = alignOnCategories(categorySources);
16902
+ const composed = composeChartSeries({
16903
+ gridSeries: aligned.series,
16904
+ length: aligned.categories.length,
16905
+ definitions: this.config.additionalSeries ?? [],
16906
+ primary: this.config.primarySeries,
16907
+ stackedChart: false,
16908
+ sourceProblems: problems,
16909
+ evaluator: this.expressionEvaluator
16910
+ });
16911
+ hints.push(...composed.hints);
16912
+ const colors = new Map((this.config.series ?? []).map(s => [s.name ?? s.field, s.color]));
16913
+ this._categories.set(this.categoryLabels(aligned.categories));
16914
+ this._seriesData.set(composed.series.map(s => ({
16915
+ name: s.name,
16916
+ data: this.hasColorThresholds() ? this.applyColorThresholds(s.data) : s.data,
16917
+ color: colors.get(s.name)
16918
+ })));
16919
+ this._sourceHints.set(hints);
16920
+ this._isLoading.set(false);
16921
+ }
16922
+ catch (err) {
16923
+ console.error('Error loading Bar Chart data:', err);
16924
+ this._error.set('Failed to load data');
16925
+ this._sourceHints.set([]);
16926
+ this._isLoading.set(false);
16927
+ }
16928
+ }
16929
+ /** Rows of one source of the multi-source path; widget filters apply to the primary query only. */
16930
+ async fetchSourceRows(source, scopeRtIds) {
16931
+ const ds = source.dataSource;
16932
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
16933
+ const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
16934
+ const streamDataArgs = timeArgs || rtIds ? { ...timeArgs, rtIds } : undefined;
16935
+ const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
16936
+ fieldFilter: source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined,
16937
+ streamDataArgs,
16938
+ scopeRtIds
16939
+ }));
16940
+ return result.rows.filter(row => BarChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
16941
+ }
16942
+ /**
16943
+ * Category labels of the multi-source path: date-times as dates when every category is a
16944
+ * different day (daily bars), else date and time as the single-query chart shows them; other
16945
+ * values as text. Repeated labels get " (2)" so every category keeps its slot.
16946
+ */
16947
+ categoryLabels(categories) {
16948
+ const mode = this.stateService.timeZoneMode();
16949
+ const allInstants = categories.length > 0 && categories.every(c => c.instant !== null);
16950
+ const dayOf = (c) => formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '';
16951
+ const byDay = allInstants && new Set(categories.map(dayOf)).size === categories.length;
16952
+ return uniqueLabels(categories.map(c => {
16953
+ if (byDay)
16954
+ return dayOf(c);
16955
+ if (c.instant !== null) {
16956
+ return formatInstant(new Date(c.instant), mode, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) ?? String(c.raw);
16957
+ }
16958
+ return String(c.raw ?? '');
16959
+ }));
16960
+ }
16961
+ /** Short message of a failed source for its hint. */
16962
+ errorMessage(reason) {
16963
+ const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
16964
+ return message ? `Failed to load data (${message})` : 'Failed to load data';
16965
+ }
16146
16966
  buildStreamDataArgs() {
16147
16967
  const ds = this.config.dataSource;
16148
16968
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
@@ -16173,15 +16993,16 @@ class BarChartWidgetComponent {
16173
16993
  for (const seriesConfig of (this.config.series ?? [])) {
16174
16994
  const seriesCell = findCellForField(row.cells, seriesConfig.field);
16175
16995
  if (seriesCell) {
16176
- const numValue = typeof seriesCell.value === 'number' ? seriesCell.value : parseFloat(String(seriesCell.value));
16177
- rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
16996
+ rowValues.set(seriesConfig.field, BarChartWidgetComponent.toBarValue(seriesCell.value));
16178
16997
  }
16179
16998
  }
16180
16999
  if (categoryValue !== '') {
16181
17000
  categories.push(categoryValue);
16182
17001
  // Add values for each series
16183
17002
  for (const seriesConfig of (this.config.series ?? [])) {
16184
- 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;
16185
17006
  seriesMap.get(seriesConfig.field)?.push(value);
16186
17007
  }
16187
17008
  }
@@ -16228,13 +17049,7 @@ class BarChartWidgetComponent {
16228
17049
  const valueCell = findCellForField(row.cells, valueField);
16229
17050
  const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
16230
17051
  const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
16231
- let numericValue = 0;
16232
- if (valueCell) {
16233
- const val = valueCell.value;
16234
- numericValue = typeof val === 'number' ? val : parseFloat(String(val));
16235
- if (isNaN(numericValue))
16236
- numericValue = 0;
16237
- }
17052
+ const numericValue = valueCell ? BarChartWidgetComponent.toBarValue(valueCell.value) : 0;
16238
17053
  if (categoryValue && seriesGroupValue) {
16239
17054
  allCategories.add(categoryValue);
16240
17055
  allSeriesGroups.add(seriesGroupValue);
@@ -16251,7 +17066,12 @@ class BarChartWidgetComponent {
16251
17066
  const valueScale = this.config?.valueScale ?? 1;
16252
17067
  const seriesData = seriesGroups.map(seriesGroup => {
16253
17068
  const rawData = categories.map(category => {
16254
- 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;
16255
17075
  });
16256
17076
  return {
16257
17077
  name: seriesGroup,
@@ -16276,11 +17096,21 @@ class BarChartWidgetComponent {
16276
17096
  applyColorThresholds(data) {
16277
17097
  const thresholds = this.config.colorThresholds ?? [];
16278
17098
  const defaultColor = this.config.defaultBarColor ?? '#6b7280';
16279
- return data.map(val => ({
17099
+ return data.map(val => val === null ? { value: null } : ({
16280
17100
  value: val,
16281
17101
  _color: this.getColorForValue(val, thresholds, defaultColor)
16282
17102
  }));
16283
17103
  }
17104
+ /**
17105
+ * Bar value of a cell: a number, `null` for a null cell ("no data" — Kendo draws no
17106
+ * bar, AB#7975), and 0 for text that is not a number.
17107
+ */
17108
+ static toBarValue(value) {
17109
+ if (value === null || value === undefined)
17110
+ return null;
17111
+ const numValue = typeof value === 'number' ? value : parseFloat(String(value));
17112
+ return isNaN(numValue) ? 0 : numValue;
17113
+ }
16284
17114
  getColorForValue(val, thresholds, defaultColor) {
16285
17115
  for (const t of thresholds) {
16286
17116
  if (val <= t.value)
@@ -16324,6 +17154,15 @@ class BarChartWidgetComponent {
16324
17154
  <span>{{ error() }}</span>
16325
17155
  </div>
16326
17156
  } @else {
17157
+ @if (sourceHints().length > 0) {
17158
+ <ul class="source-hints" aria-live="polite">
17159
+ @for (hint of sourceHints(); track hint.text) {
17160
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
17161
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
17162
+ </li>
17163
+ }
17164
+ </ul>
17165
+ }
16327
17166
  <kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
16328
17167
  <kendo-chart-area [background]="'transparent'"></kendo-chart-area>
16329
17168
  <kendo-chart-category-axis>
@@ -16376,18 +17215,32 @@ class BarChartWidgetComponent {
16376
17215
  [labels]="{ color: chartTheme().text }">
16377
17216
  </kendo-chart-legend>
16378
17217
 
16379
- <kendo-chart-tooltip>
16380
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
16381
- <div class="chart-tooltip">
16382
- <strong>{{ category }}</strong><br/>
16383
- {{ series.name }}: {{ formatValue(value) }}
16384
- </div>
16385
- </ng-template>
16386
- </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
+ }
16387
17240
  </kendo-chart>
16388
17241
  }
16389
17242
  </div>
16390
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17243
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SeriesTooltipTemplateDirective, selector: "[kendoChartSeriesTooltipTemplate]" }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.SeriesLabelsComponent, selector: "kendo-chart-series-item-labels", inputs: ["align", "background", "border", "color", "content", "ariaContent", "distance", "font", "format", "margin", "padding", "position", "rotation", "visible", "visual", "from", "to"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
16391
17244
  }
16392
17245
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartWidgetComponent, decorators: [{
16393
17246
  type: Component,
@@ -16408,6 +17261,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
16408
17261
  <span>{{ error() }}</span>
16409
17262
  </div>
16410
17263
  } @else {
17264
+ @if (sourceHints().length > 0) {
17265
+ <ul class="source-hints" aria-live="polite">
17266
+ @for (hint of sourceHints(); track hint.text) {
17267
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'dependency'">
17268
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
17269
+ </li>
17270
+ }
17271
+ </ul>
17272
+ }
16411
17273
  <kendo-chart class="chart-container" [plotArea]="{ background: 'transparent', margin: plotAreaMargin() }">
16412
17274
  <kendo-chart-area [background]="'transparent'"></kendo-chart-area>
16413
17275
  <kendo-chart-category-axis>
@@ -16460,22 +17322,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
16460
17322
  [labels]="{ color: chartTheme().text }">
16461
17323
  </kendo-chart-legend>
16462
17324
 
16463
- <kendo-chart-tooltip>
16464
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
16465
- <div class="chart-tooltip">
16466
- <strong>{{ category }}</strong><br/>
16467
- {{ series.name }}: {{ formatValue(value) }}
16468
- </div>
16469
- </ng-template>
16470
- </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
+ }
16471
17347
  </kendo-chart>
16472
17348
  }
16473
17349
  </div>
16474
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
17350
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.bar-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.bar-chart-widget.loading,.bar-chart-widget.error{opacity:.7}.loading-indicator,.error-message,.no-config-overlay{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.no-config-overlay span{color:var(--kendo-color-subtle, #6c757d);font-style:italic}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 12px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}\n"] }]
16475
17351
  }], propDecorators: { config: [{
16476
17352
  type: Input
16477
17353
  }] } });
16478
17354
 
17355
+ const NUMERIC_TYPES = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
17356
+ /**
17357
+ * Series section of the line chart config dialog (AB#7951, wireframe linked in AB#7951): the
17358
+ * series that come after the primary query's series, in legend / stacking order.
17359
+ *
17360
+ * - Query rows: own stream-data query, value field, entity-selector scope, time-filter opt-out and
17361
+ * resolution-aware routing (AB#7951).
17362
+ * - Formula rows (AB#7952): a formula over the other rows' keys in the shared formula field
17363
+ * (series keys as available names, no preview).
17364
+ * - Every row and the primary row: Visible and Stacked (Stacked only in a stacked area chart;
17365
+ * unchecked = drawn as a line over the stack).
17366
+ *
17367
+ * Rows can be added, removed and moved by keyboard; the focus stays on a moved row's button.
17368
+ *
17369
+ * `alignment="category"` (bar chart, AB#7953): rows are matched per category value, so every query
17370
+ * row gets its own category field; Resolution-aware and Stacked are not shown (the chart type
17371
+ * stacks).
17372
+ */
17373
+ class LineChartSeriesEditorComponent {
17374
+ queryExecutor = inject(QueryExecutorService);
17375
+ stateService = inject(MeshBoardStateService);
17376
+ cdr = inject(ChangeDetectorRef);
17377
+ host = inject(ElementRef);
17378
+ evaluator = inject(ExpressionEvaluatorService);
17379
+ /** The series (query and formula rows), in order. */
17380
+ series = [];
17381
+ /** Display options of the primary query's series (AB#7952). */
17382
+ primary;
17383
+ /** The chart stacks (area + stacked): enables the per-series Stacked checkboxes (AB#7952). */
17384
+ stackedChart = false;
17385
+ /** Emits the primary display options on change; `undefined` when both are default. */
17386
+ primaryChange = new EventEmitter();
17387
+ /** Entity selectors of the board (scope picker). */
17388
+ selectors = [];
17389
+ /**
17390
+ * How the chart matches series (AB#7953): `timeGrid` (line chart, default) or `category` (bar
17391
+ * chart: per-row category field, no Resolution-aware, no Stacked).
17392
+ */
17393
+ alignment = 'timeGrid';
17394
+ /** Category field preset for a new query row in category mode (the primary query's category field). */
17395
+ defaultCategoryField;
17396
+ /** Emits a new array on every change. */
17397
+ seriesChange = new EventEmitter();
17398
+ rowCards;
17399
+ get arrowUpIcon() { return arrowUpIcon; }
17400
+ get arrowDownIcon() { return arrowDownIcon; }
17401
+ get plusIcon() { return plusIcon; }
17402
+ get trashIcon() { return MM_ACTION_ICONS.delete; }
17403
+ streamDataOnly = ['streamData'];
17404
+ stackedDisabledHint = 'Only a stacked area chart stacks series.';
17405
+ keySyntaxHint = '${key}';
17406
+ formulaPlaceholder = 'e.g. min(${pv} - ${gridExport}, ${consumption})';
17407
+ aggregationOptions = [
17408
+ { value: 'SUM', label: 'Sum' },
17409
+ { value: 'AVG', label: 'Average' },
17410
+ { value: 'MAX', label: 'Max' },
17411
+ { value: 'MIN', label: 'Min' },
17412
+ { value: 'COUNT', label: 'Count' },
17413
+ { value: 'TIME_WEIGHTED_AVG', label: 'Time-weighted average' }
17414
+ ];
17415
+ /** Rows are matched per category value (bar chart, AB#7953). */
17416
+ get categoryMode() {
17417
+ return this.alignment === 'category';
17418
+ }
17419
+ rows = [];
17420
+ errors = [];
17421
+ nextUid = 1;
17422
+ operandCache = new Map();
17423
+ /** The array last emitted, so an echo through `[series]` does not rebuild the rows. */
17424
+ lastEmitted = null;
17425
+ ngOnChanges(changes) {
17426
+ if (changes['series'] && this.series !== this.lastEmitted) {
17427
+ this.rows = (this.series ?? []).map(s => this.createRow(structuredClone(s)));
17428
+ this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
17429
+ for (const row of this.rows) {
17430
+ if (row.series.kind === 'query' && row.series.dataSource.queryRtId)
17431
+ void this.loadColumns(row);
17432
+ }
17433
+ }
17434
+ else if (changes['alignment']) {
17435
+ this.errors = querySeriesErrors(this.rows.map(r => r.series), this.evaluator, this.validationOptions());
17436
+ }
17437
+ }
17438
+ add() {
17439
+ this.rows = [...this.rows, this.createRow({
17440
+ kind: 'query',
17441
+ key: '',
17442
+ dataSource: { type: 'persistentQuery', queryRtId: '', queryFamily: 'streamData' },
17443
+ valueField: ''
17444
+ })];
17445
+ this.emit();
17446
+ this.focusAfterRender(this.rows.length - 1, 'key');
17447
+ }
17448
+ addFormula() {
17449
+ this.rows = [...this.rows, this.createRow({ kind: 'formula', key: '', formula: '' })];
17450
+ this.emit();
17451
+ this.focusAfterRender(this.rows.length - 1, 'key');
17452
+ }
17453
+ /** The row as query series (template narrowing). */
17454
+ asQuery(row) {
17455
+ return row.series.kind === 'query' ? row.series : null;
17456
+ }
17457
+ /** The row as formula series (template narrowing). */
17458
+ asFormula(row) {
17459
+ return row.series.kind === 'formula' ? row.series : null;
17460
+ }
17461
+ /**
17462
+ * Names a formula row can insert: the keys of every other row (a self-reference would be a
17463
+ * circular reference). Cached per key set, so the formula field's inputs keep their reference.
17464
+ */
17465
+ operandsFor(row) {
17466
+ const names = this.rows
17467
+ .filter(r => r !== row && SERIES_KEY_PATTERN.test(r.series.key?.trim() ?? ''))
17468
+ .map(r => r.series.key.trim());
17469
+ const cacheKey = names.join('\u0000');
17470
+ const cached = this.operandCache.get(row.uid);
17471
+ if (cached && cached.key === cacheKey)
17472
+ return cached.variables;
17473
+ const variables = names.map(name => ({ name, type: 'number', source: 'static', value: '' }));
17474
+ this.operandCache.set(row.uid, { key: cacheKey, variables });
17475
+ return variables;
17476
+ }
17477
+ /**
17478
+ * Message under a row. For a formula row the shared formula field already shows syntax and
17479
+ * unknown-key messages; the row repeats only what the field cannot know (key, circular reference).
17480
+ */
17481
+ rowMessage(index) {
17482
+ const message = this.errors[index] ?? null;
17483
+ const series = this.rows[index]?.series;
17484
+ if (!message || series?.kind !== 'formula' || !series.formula?.trim())
17485
+ return message;
17486
+ const keys = this.rows.map(r => r.series.key?.trim() ?? '').filter(k => !!k);
17487
+ const fieldError = validateFormula(series.formula, keys, this.evaluator).error;
17488
+ return message === fieldError ? null : message;
17489
+ }
17490
+ setOption(row, option, on) {
17491
+ const next = { ...row.series };
17492
+ if (on)
17493
+ delete next[option];
17494
+ else
17495
+ next[option] = false;
17496
+ row.series = next;
17497
+ this.emit();
17498
+ }
17499
+ setPrimaryOption(option, on) {
17500
+ const next = { ...(this.primary ?? {}) };
17501
+ if (on)
17502
+ delete next[option];
17503
+ else
17504
+ next[option] = false;
17505
+ this.primary = Object.keys(next).length > 0 ? next : undefined;
17506
+ this.primaryChange.emit(this.primary);
17507
+ this.cdr.markForCheck();
17508
+ }
17509
+ remove(index) {
17510
+ this.rows = this.rows.filter((_, i) => i !== index);
17511
+ this.emit();
17512
+ if (this.rows.length > 0)
17513
+ this.focusAfterRender(Math.min(index, this.rows.length - 1), 'down');
17514
+ else
17515
+ this.focusAdd();
17516
+ }
17517
+ move(index, delta) {
17518
+ const target = index + delta;
17519
+ if (target < 0 || target >= this.rows.length)
17520
+ return;
17521
+ const rows = [...this.rows];
17522
+ [rows[index], rows[target]] = [rows[target], rows[index]];
17523
+ this.rows = rows;
17524
+ this.emit();
17525
+ // Keep the focus on the moved row: on its button in the same direction, or the other one at an end.
17526
+ const atEnd = delta < 0 ? target === 0 : target === rows.length - 1;
17527
+ this.focusAfterRender(target, atEnd ? (delta < 0 ? 'down' : 'up') : (delta < 0 ? 'up' : 'down'));
17528
+ }
17529
+ async onQuerySelected(row, query) {
17530
+ if (row.series.kind !== 'query')
17531
+ return;
17532
+ row.query = query;
17533
+ row.series.dataSource = {
17534
+ ...row.series.dataSource,
17535
+ queryRtId: query?.rtId ?? '',
17536
+ queryName: query?.name || undefined,
17537
+ queryFamily: 'streamData'
17538
+ };
17539
+ row.series.valueField = '';
17540
+ if (this.categoryMode)
17541
+ row.series.categoryField = '';
17542
+ row.numericColumns = [];
17543
+ row.allColumns = [];
17544
+ this.emit();
17545
+ if (query) {
17546
+ await this.loadColumns(row);
17547
+ this.presetCategoryField(row);
17548
+ }
17549
+ }
17550
+ /**
17551
+ * Category mode: a row without a category field gets the primary query's category field when
17552
+ * its query has that column, else `timestamp` when it has one (AB#7953).
17553
+ */
17554
+ presetCategoryField(row) {
17555
+ if (!this.categoryMode || row.series.kind !== 'query' || row.series.categoryField)
17556
+ return;
17557
+ const paths = row.allColumns.map(c => c.attributePath);
17558
+ const preset = [this.defaultCategoryField, 'timestamp'].find(f => !!f && paths.includes(f));
17559
+ if (preset) {
17560
+ row.series.categoryField = preset;
17561
+ this.emit();
17562
+ }
17563
+ }
17564
+ validationOptions() {
17565
+ return { requireCategoryField: this.categoryMode };
17566
+ }
17567
+ onResolutionAwareChange(row, on) {
17568
+ if (row.series.kind !== 'query')
17569
+ return;
17570
+ row.series.dataSource = {
17571
+ ...row.series.dataSource,
17572
+ resolutionAware: on || undefined,
17573
+ requiredAggregation: on ? (row.series.dataSource.requiredAggregation ?? 'AVG') : undefined
17574
+ };
17575
+ this.emit();
17576
+ }
17577
+ emit() {
17578
+ const series = this.rows.map(r => ({ ...r.series, key: r.series.key?.trim() ?? '', label: r.series.label?.trim() || undefined }));
17579
+ this.errors = querySeriesErrors(series, this.evaluator, this.validationOptions());
17580
+ this.lastEmitted = series;
17581
+ this.seriesChange.emit(series);
17582
+ this.cdr.markForCheck();
17583
+ }
17584
+ createRow(series) {
17585
+ const ds = series.kind === 'query' ? series.dataSource : undefined;
17586
+ return {
17587
+ uid: this.nextUid++,
17588
+ series,
17589
+ query: ds?.queryRtId ? { rtId: ds.queryRtId, name: ds.queryName || ds.queryRtId } : null,
17590
+ numericColumns: [],
17591
+ allColumns: [],
17592
+ loadingColumns: false
17593
+ };
17594
+ }
17595
+ /** Numeric columns of the row's query, probed with the board window (AB#7975). */
17596
+ async loadColumns(row) {
17597
+ if (row.series.kind !== 'query')
17598
+ return;
17599
+ const queryRtId = row.series.dataSource.queryRtId;
17600
+ row.loadingColumns = true;
17601
+ this.cdr.markForCheck();
17602
+ try {
17603
+ const columns = await this.queryExecutor.fetchStreamDataColumns(queryRtId, columnProbeWindow(this.stateService.resolveStreamDataTimeArgs()));
17604
+ row.allColumns = columns
17605
+ .map(c => ({ attributePath: c.attributePath, attributeValueType: c.attributeValueType ?? '', aggregationType: c.aggregationType ?? null }));
17606
+ row.numericColumns = row.allColumns.filter(c => NUMERIC_TYPES.includes(c.attributeValueType));
17607
+ }
17608
+ catch (error) {
17609
+ console.warn('Could not load the columns of the series query:', error);
17610
+ row.numericColumns = [];
17611
+ row.allColumns = [];
17612
+ }
17613
+ finally {
17614
+ row.loadingColumns = false;
17615
+ this.cdr.markForCheck();
17616
+ }
17617
+ }
17618
+ focusAfterRender(index, target) {
17619
+ setTimeout(() => {
17620
+ const card = this.rowCards?.get(index)?.nativeElement;
17621
+ const selector = target === 'key' ? `[data-series-key="${index}"] input` : `[data-series-action="${target}"]`;
17622
+ const el = card?.querySelector(selector);
17623
+ (el && !el.disabled ? el : card)?.focus();
17624
+ });
17625
+ }
17626
+ focusAdd() {
17627
+ setTimeout(() => this.host.nativeElement.querySelector('[data-series-action="add"]')?.focus());
17628
+ }
17629
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17630
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartSeriesEditorComponent, isStandalone: true, selector: "mm-line-chart-series-editor", inputs: { series: "series", primary: "primary", stackedChart: "stackedChart", selectors: "selectors", alignment: "alignment", defaultCategoryField: "defaultCategoryField" }, outputs: { primaryChange: "primaryChange", seriesChange: "seriesChange" }, viewQueries: [{ propertyName: "rowCards", predicate: ["rowCard"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
17631
+ <div class="series-editor">
17632
+ <div class="series-row primary" role="group" aria-label="Series of the primary query">
17633
+ <span class="tag">primary</span>
17634
+ <span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
17635
+ <span class="display-options">
17636
+ <label class="checkbox">
17637
+ <input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
17638
+ (ngModelChange)="setPrimaryOption('visible', $event)" />
17639
+ Visible
17640
+ </label>
17641
+ @if (!categoryMode) {
17642
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17643
+ <input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
17644
+ [ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
17645
+ Stacked
17646
+ </label>
17647
+ }
17648
+ </span>
17649
+ </div>
17650
+
17651
+ @for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
17652
+ <div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
17653
+ <div class="row-actions">
17654
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
17655
+ data-series-action="up" [disabled]="first"
17656
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
17657
+ (click)="move(i, -1)"></button>
17658
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
17659
+ data-series-action="down" [disabled]="last"
17660
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
17661
+ (click)="move(i, 1)"></button>
17662
+ <span class="tag">{{ row.series.kind }}</span>
17663
+ </div>
17664
+
17665
+ <div class="field key">
17666
+ <label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
17667
+ <kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
17668
+ [(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
17669
+ </div>
17670
+ <div class="field label">
17671
+ <label [for]="'series-label-' + row.uid">Label</label>
17672
+ <kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
17673
+ (valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
17674
+ </div>
17675
+ @if (asQuery(row); as q) {
17676
+ <div class="field query">
17677
+ <label>Query (stream data) <span class="required">*</span></label>
17678
+ <mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
17679
+ (querySelected)="onQuerySelected(row, $event)"
17680
+ placeholder="Select a stream-data query..."></mm-query-selector>
17681
+ </div>
17682
+ @if (categoryMode) {
17683
+ <div class="field category">
17684
+ <label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
17685
+ <kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
17686
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17687
+ [loading]="row.loadingColumns" [allowCustom]="true"
17688
+ [(ngModel)]="q.categoryField" (valueChange)="emit()"
17689
+ placeholder="e.g. timestamp"></kendo-combobox>
17690
+ </div>
17691
+ }
17692
+ <div class="field value">
17693
+ <label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
17694
+ <kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
17695
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17696
+ [loading]="row.loadingColumns" [allowCustom]="true"
17697
+ [(ngModel)]="q.valueField" (valueChange)="emit()"
17698
+ placeholder="Numeric column..."></kendo-combobox>
17699
+ </div>
17700
+ <div class="field scope">
17701
+ <mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
17702
+ [entitySelectorId]="q.dataSource.entitySelectorId"
17703
+ (entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
17704
+ </mm-entity-selector-scope-picker>
17705
+ </div>
17706
+ <div class="field toggles">
17707
+ <mm-sd-time-filter-toggle [family]="'streamData'"
17708
+ [ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
17709
+ (ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
17710
+ </mm-sd-time-filter-toggle>
17711
+ @if (!categoryMode) {
17712
+ <label class="checkbox">
17713
+ <input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
17714
+ (ngModelChange)="onResolutionAwareChange(row, $event)" />
17715
+ Resolution-aware
17716
+ </label>
17717
+ }
17718
+ @if (!categoryMode && q.dataSource.resolutionAware) {
17719
+ <kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
17720
+ [valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
17721
+ [(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
17722
+ </kendo-dropdownlist>
17723
+ }
17724
+ </div>
17725
+ } @else if (asFormula(row); as f) {
17726
+ <div class="field formula">
17727
+ <mm-meshboard-formula-field
17728
+ [formula]="f.formula"
17729
+ (formulaChange)="f.formula = $event; emit()"
17730
+ [variables]="operandsFor(row)"
17731
+ [showPreview]="false"
17732
+ label="Formula"
17733
+ variablesLabel="Available series"
17734
+ namesNoun="series"
17735
+ [syntaxHint]="keySyntaxHint"
17736
+ emptyVariablesText="No other series yet. Add query series first."
17737
+ [placeholder]="formulaPlaceholder">
17738
+ </mm-meshboard-formula-field>
17739
+ </div>
17740
+ }
17741
+ <div class="field display-options">
17742
+ <label class="checkbox">
17743
+ <input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
17744
+ (ngModelChange)="setOption(row, 'visible', $event)" />
17745
+ Visible
17746
+ </label>
17747
+ @if (!categoryMode) {
17748
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17749
+ <input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
17750
+ [ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
17751
+ Stacked
17752
+ </label>
17753
+ }
17754
+ </div>
17755
+ <button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
17756
+ data-series-action="remove"
17757
+ [attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
17758
+ (click)="remove(i)"></button>
17759
+ @if (rowMessage(i); as message) {
17760
+ <p class="row-error" role="alert">{{ message }}</p>
17761
+ }
17762
+ </div>
17763
+ }
17764
+
17765
+ <div class="add-actions">
17766
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
17767
+ Add query series
17768
+ </button>
17769
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
17770
+ Add formula series
17771
+ </button>
17772
+ </div>
17773
+ </div>
17774
+ `, isInline: true, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: FormulaFieldComponent, selector: "mm-meshboard-formula-field", inputs: ["formula", "variables", "declaredNames", "numericOnly", "clampHint", "label", "showPreview", "variablesLabel", "namesNoun", "syntaxHint", "emptyVariablesText", "placeholder"], outputs: ["formulaChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
17775
+ }
17776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartSeriesEditorComponent, decorators: [{
17777
+ type: Component,
17778
+ args: [{ selector: 'mm-line-chart-series-editor', standalone: true, imports: [
17779
+ FormsModule,
17780
+ ButtonModule,
17781
+ DropDownsModule,
17782
+ InputsModule,
17783
+ QuerySelectorComponent,
17784
+ SdTimeFilterToggleComponent,
17785
+ EntitySelectorScopePickerComponent,
17786
+ FormulaFieldComponent
17787
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
17788
+ <div class="series-editor">
17789
+ <div class="series-row primary" role="group" aria-label="Series of the primary query">
17790
+ <span class="tag">primary</span>
17791
+ <span>{{ categoryMode ? 'Series of the primary query, edited above' : 'Series of the primary query (one per series group value), edited above' }}</span>
17792
+ <span class="display-options">
17793
+ <label class="checkbox">
17794
+ <input type="checkbox" kendoCheckBox data-series-primary="visible" [ngModel]="primary?.visible !== false"
17795
+ (ngModelChange)="setPrimaryOption('visible', $event)" />
17796
+ Visible
17797
+ </label>
17798
+ @if (!categoryMode) {
17799
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17800
+ <input type="checkbox" kendoCheckBox data-series-primary="stacked" [disabled]="!stackedChart"
17801
+ [ngModel]="primary?.stacked !== false" (ngModelChange)="setPrimaryOption('stacked', $event)" />
17802
+ Stacked
17803
+ </label>
17804
+ }
17805
+ </span>
17806
+ </div>
17807
+
17808
+ @for (row of rows; track row.uid; let i = $index; let first = $first; let last = $last) {
17809
+ <div class="series-row" #rowCard tabindex="-1" role="group" [attr.aria-label]="'Series ' + (row.series.key || (i + 1))">
17810
+ <div class="row-actions">
17811
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowUpIcon"
17812
+ data-series-action="up" [disabled]="first"
17813
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' up'" title="Move up"
17814
+ (click)="move(i, -1)"></button>
17815
+ <button kendoButton type="button" fillMode="flat" size="small" [svgIcon]="arrowDownIcon"
17816
+ data-series-action="down" [disabled]="last"
17817
+ [attr.aria-label]="'Move series ' + (row.series.key || (i + 1)) + ' down'" title="Move down"
17818
+ (click)="move(i, 1)"></button>
17819
+ <span class="tag">{{ row.series.kind }}</span>
17820
+ </div>
17821
+
17822
+ <div class="field key">
17823
+ <label [for]="'series-key-' + row.uid">Key <span class="required">*</span></label>
17824
+ <kendo-textbox [id]="'series-key-' + row.uid" [attr.data-series-key]="i"
17825
+ [(ngModel)]="row.series.key" (valueChange)="emit()" placeholder="e.g. pv"></kendo-textbox>
17826
+ </div>
17827
+ <div class="field label">
17828
+ <label [for]="'series-label-' + row.uid">Label</label>
17829
+ <kendo-textbox [id]="'series-label-' + row.uid" [(ngModel)]="row.series.label"
17830
+ (valueChange)="emit()" placeholder="Legend label"></kendo-textbox>
17831
+ </div>
17832
+ @if (asQuery(row); as q) {
17833
+ <div class="field query">
17834
+ <label>Query (stream data) <span class="required">*</span></label>
17835
+ <mm-query-selector [ngModel]="row.query" [acceptFamilies]="streamDataOnly"
17836
+ (querySelected)="onQuerySelected(row, $event)"
17837
+ placeholder="Select a stream-data query..."></mm-query-selector>
17838
+ </div>
17839
+ @if (categoryMode) {
17840
+ <div class="field category">
17841
+ <label [for]="'series-category-' + row.uid">Category field <span class="required">*</span></label>
17842
+ <kendo-combobox [id]="'series-category-' + row.uid" [attr.data-series-category]="i" [data]="row.allColumns"
17843
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17844
+ [loading]="row.loadingColumns" [allowCustom]="true"
17845
+ [(ngModel)]="q.categoryField" (valueChange)="emit()"
17846
+ placeholder="e.g. timestamp"></kendo-combobox>
17847
+ </div>
17848
+ }
17849
+ <div class="field value">
17850
+ <label [for]="'series-value-' + row.uid">Value field <span class="required">*</span></label>
17851
+ <kendo-combobox [id]="'series-value-' + row.uid" [data]="row.numericColumns"
17852
+ [textField]="'attributePath'" [valueField]="'attributePath'" [valuePrimitive]="true"
17853
+ [loading]="row.loadingColumns" [allowCustom]="true"
17854
+ [(ngModel)]="q.valueField" (valueChange)="emit()"
17855
+ placeholder="Numeric column..."></kendo-combobox>
17856
+ </div>
17857
+ <div class="field scope">
17858
+ <mm-entity-selector-scope-picker [family]="'streamData'" [selectors]="selectors"
17859
+ [entitySelectorId]="q.dataSource.entitySelectorId"
17860
+ (entitySelectorIdChange)="q.dataSource.entitySelectorId = $event; emit()">
17861
+ </mm-entity-selector-scope-picker>
17862
+ </div>
17863
+ <div class="field toggles">
17864
+ <mm-sd-time-filter-toggle [family]="'streamData'"
17865
+ [ignoreTimeFilter]="!!q.dataSource.ignoreTimeFilter"
17866
+ (ignoreTimeFilterChange)="q.dataSource.ignoreTimeFilter = $event || undefined; emit()">
17867
+ </mm-sd-time-filter-toggle>
17868
+ @if (!categoryMode) {
17869
+ <label class="checkbox">
17870
+ <input type="checkbox" kendoCheckBox [ngModel]="!!q.dataSource.resolutionAware"
17871
+ (ngModelChange)="onResolutionAwareChange(row, $event)" />
17872
+ Resolution-aware
17873
+ </label>
17874
+ }
17875
+ @if (!categoryMode && q.dataSource.resolutionAware) {
17876
+ <kendo-dropdownlist [data]="aggregationOptions" [textField]="'label'" [valueField]="'value'"
17877
+ [valuePrimitive]="true" [attr.aria-label]="'Aggregation'"
17878
+ [(ngModel)]="q.dataSource.requiredAggregation" (valueChange)="emit()">
17879
+ </kendo-dropdownlist>
17880
+ }
17881
+ </div>
17882
+ } @else if (asFormula(row); as f) {
17883
+ <div class="field formula">
17884
+ <mm-meshboard-formula-field
17885
+ [formula]="f.formula"
17886
+ (formulaChange)="f.formula = $event; emit()"
17887
+ [variables]="operandsFor(row)"
17888
+ [showPreview]="false"
17889
+ label="Formula"
17890
+ variablesLabel="Available series"
17891
+ namesNoun="series"
17892
+ [syntaxHint]="keySyntaxHint"
17893
+ emptyVariablesText="No other series yet. Add query series first."
17894
+ [placeholder]="formulaPlaceholder">
17895
+ </mm-meshboard-formula-field>
17896
+ </div>
17897
+ }
17898
+ <div class="field display-options">
17899
+ <label class="checkbox">
17900
+ <input type="checkbox" kendoCheckBox [attr.data-series-visible]="i" [ngModel]="row.series.visible !== false"
17901
+ (ngModelChange)="setOption(row, 'visible', $event)" />
17902
+ Visible
17903
+ </label>
17904
+ @if (!categoryMode) {
17905
+ <label class="checkbox" [title]="stackedChart ? '' : stackedDisabledHint">
17906
+ <input type="checkbox" kendoCheckBox [attr.data-series-stacked]="i" [disabled]="!stackedChart"
17907
+ [ngModel]="row.series.stacked !== false" (ngModelChange)="setOption(row, 'stacked', $event)" />
17908
+ Stacked
17909
+ </label>
17910
+ }
17911
+ </div>
17912
+ <button kendoButton type="button" fillMode="flat" size="small" class="remove" [svgIcon]="trashIcon"
17913
+ data-series-action="remove"
17914
+ [attr.aria-label]="'Remove series ' + (row.series.key || (i + 1))" title="Remove series"
17915
+ (click)="remove(i)"></button>
17916
+ @if (rowMessage(i); as message) {
17917
+ <p class="row-error" role="alert">{{ message }}</p>
17918
+ }
17919
+ </div>
17920
+ }
17921
+
17922
+ <div class="add-actions">
17923
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add" (click)="add()">
17924
+ Add query series
17925
+ </button>
17926
+ <button kendoButton type="button" fillMode="flat" [svgIcon]="plusIcon" data-series-action="add-formula" (click)="addFormula()">
17927
+ Add formula series
17928
+ </button>
17929
+ </div>
17930
+ </div>
17931
+ `, styles: [".series-editor{display:flex;flex-direction:column;gap:8px}.series-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px 12px;padding:8px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #fff)}.series-row.primary{align-items:center;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-base-subtle, transparent)}.row-actions{display:flex;align-items:center;gap:2px}.tag{font-size:.7rem;padding:0 6px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:8px;color:var(--kendo-color-subtle, #6c757d)}.field{display:flex;flex-direction:column;gap:4px;min-width:0}.field label{font-size:.8rem;font-weight:600}.field.key{flex:0 1 120px}.field.label{flex:1 1 140px}.field.query{flex:2 1 220px}.field.value,.field.category{flex:1 1 160px}.field.scope{flex:1 1 200px}.field.formula{flex:1 1 100%}.field.display-options,.display-options{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.add-actions{display:flex;flex-wrap:wrap;gap:8px}.field.toggles{flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px 16px}.checkbox{display:flex;align-items:center;gap:6px;font-weight:400;font-size:.85rem}.remove{margin-left:auto}.required{color:var(--kendo-color-error, #dc3545)}.row-error{flex-basis:100%;margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}\n"] }]
17932
+ }], propDecorators: { series: [{
17933
+ type: Input
17934
+ }], primary: [{
17935
+ type: Input
17936
+ }], stackedChart: [{
17937
+ type: Input
17938
+ }], primaryChange: [{
17939
+ type: Output
17940
+ }], selectors: [{
17941
+ type: Input
17942
+ }], alignment: [{
17943
+ type: Input
17944
+ }], defaultCategoryField: [{
17945
+ type: Input
17946
+ }], seriesChange: [{
17947
+ type: Output
17948
+ }], rowCards: [{
17949
+ type: ViewChildren,
17950
+ args: ['rowCard']
17951
+ }] } });
17952
+
16479
17953
  /**
16480
17954
  * Configuration dialog for Bar Chart widgets.
16481
17955
  * Supports column, bar, stacked, and 100% stacked chart types.
@@ -16485,6 +17959,7 @@ class BarChartConfigDialogComponent {
16485
17959
  queryExecutor = inject(QueryExecutorService);
16486
17960
  stateService = inject(MeshBoardStateService);
16487
17961
  windowRef = inject(WindowRef);
17962
+ expressionEvaluator = inject(ExpressionEvaluatorService);
16488
17963
  querySelector;
16489
17964
  // Initial values for editing
16490
17965
  initialQueryRtId;
@@ -16503,6 +17978,16 @@ class BarChartConfigDialogComponent {
16503
17978
  initialColorThresholds;
16504
17979
  initialDefaultBarColor;
16505
17980
  initialFilters;
17981
+ initialAdditionalSeries;
17982
+ initialPrimarySeries;
17983
+ /** Query and formula series of the Series section (AB#7953). */
17984
+ additionalSeries = [];
17985
+ /** Display options of the primary query's series (AB#7953). */
17986
+ primarySeries;
17987
+ /** The series that are saved: none unless the primary query is a stream-data query. */
17988
+ get effectiveAdditionalSeries() {
17989
+ return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
17990
+ }
16506
17991
  // State
16507
17992
  isLoadingInitial = false;
16508
17993
  isLoadingColumns = false;
@@ -16558,6 +18043,10 @@ class BarChartConfigDialogComponent {
16558
18043
  if (this.selectedPersistentQuery === null || this.form.categoryField === '') {
16559
18044
  return false;
16560
18045
  }
18046
+ const seriesValid = querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator, { requireCategoryField: true })
18047
+ .every(e => e === null);
18048
+ if (!seriesValid)
18049
+ return false;
16561
18050
  if (this.seriesMode === 'dynamic') {
16562
18051
  return this.seriesGroupField !== '' && this.valueField !== '';
16563
18052
  }
@@ -16582,6 +18071,8 @@ class BarChartConfigDialogComponent {
16582
18071
  this.form.showDataLabels = this.initialShowDataLabels ?? false;
16583
18072
  this.form.colorThresholds = this.initialColorThresholds ? [...this.initialColorThresholds] : [];
16584
18073
  this.form.defaultBarColor = this.initialDefaultBarColor ?? '';
18074
+ this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
18075
+ this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
16585
18076
  // Determine series mode from initial values
16586
18077
  if (this.initialSeriesGroupField && this.initialValueField) {
16587
18078
  this.seriesMode = 'dynamic';
@@ -16689,8 +18180,10 @@ class BarChartConfigDialogComponent {
16689
18180
  aggregationType: c.aggregationType ?? null
16690
18181
  }));
16691
18182
  }
16692
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
16693
- 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 => ({
16694
18187
  attributePath: c.attributePath,
16695
18188
  attributeValueType: c.attributeValueType ?? '',
16696
18189
  aggregationType: c.aggregationType ?? null
@@ -16742,7 +18235,9 @@ class BarChartConfigDialogComponent {
16742
18235
  showDataLabels: this.form.showDataLabels,
16743
18236
  colorThresholds: this.form.colorThresholds.length > 0 ? this.form.colorThresholds : undefined,
16744
18237
  defaultBarColor: this.form.defaultBarColor || undefined,
16745
- filters: filtersDto
18238
+ filters: filtersDto,
18239
+ additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
18240
+ primarySeries: this.selectedQueryFamily === 'streamData' && this.primarySeries?.visible === false ? { visible: false } : undefined
16746
18241
  };
16747
18242
  // Add dynamic series fields if in dynamic mode
16748
18243
  if (this.seriesMode === 'dynamic') {
@@ -16761,7 +18256,7 @@ class BarChartConfigDialogComponent {
16761
18256
  this.windowRef.close();
16762
18257
  }
16763
18258
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16764
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
18259
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: BarChartConfigDialogComponent, isStandalone: true, selector: "mm-bar-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeries: "initialSeries", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowDataLabels: "initialShowDataLabels", initialColorThresholds: "initialColorThresholds", initialDefaultBarColor: "initialDefaultBarColor", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
16765
18260
  <div class="config-container">
16766
18261
 
16767
18262
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -16900,6 +18395,23 @@ class BarChartConfigDialogComponent {
16900
18395
  </div>
16901
18396
  }
16902
18397
 
18398
+ <!-- Series Section (AB#7953): query and formula series matched per category -->
18399
+ @if (selectedQueryFamily === 'streamData') {
18400
+ <div class="config-section">
18401
+ <h3 class="section-title">Series</h3>
18402
+ <p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
18403
+ <mm-line-chart-series-editor
18404
+ alignment="category"
18405
+ [series]="additionalSeries"
18406
+ [selectors]="availableEntitySelectors"
18407
+ [primary]="primarySeries"
18408
+ [defaultCategoryField]="form.categoryField"
18409
+ (seriesChange)="additionalSeries = $event"
18410
+ (primaryChange)="primarySeries = $event">
18411
+ </mm-line-chart-series-editor>
18412
+ </div>
18413
+ }
18414
+
16903
18415
  <!-- Filters Section -->
16904
18416
  @if (selectedPersistentQuery?.queryCkTypeId) {
16905
18417
  <div class="config-section">
@@ -17033,7 +18545,7 @@ class BarChartConfigDialogComponent {
17033
18545
  </button>
17034
18546
  </div>
17035
18547
  </div>
17036
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18548
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px}.checkbox-field{flex-direction:row;align-items:center}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.threshold-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}.threshold-row label{font-size:.85rem;min-width:70px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: i4.MultiSelectComponent, selector: "kendo-multiselect", inputs: ["showStickyHeader", "focusableId", "autoClose", "loading", "data", "value", "valueField", "textField", "tabindex", "tabIndex", "size", "rounded", "fillMode", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "disabled", "itemDisabled", "checkboxes", "readonly", "filterable", "virtual", "popupSettings", "listHeight", "valuePrimitive", "clearButton", "tagMapper", "allowCustom", "valueNormalizer", "inputAttributes"], outputs: ["filterChange", "valueChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "removeTag"], exportAs: ["kendoMultiSelect"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
17037
18549
  }
17038
18550
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: BarChartConfigDialogComponent, decorators: [{
17039
18551
  type: Component,
@@ -17048,7 +18560,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17048
18560
  QuerySelectorComponent,
17049
18561
  SdTimeFilterToggleComponent,
17050
18562
  EntitySelectorScopePickerComponent,
17051
- LoadingOverlayComponent
18563
+ LoadingOverlayComponent,
18564
+ LineChartSeriesEditorComponent
17052
18565
  ], template: `
17053
18566
  <div class="config-container">
17054
18567
 
@@ -17188,6 +18701,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17188
18701
  </div>
17189
18702
  }
17190
18703
 
18704
+ <!-- Series Section (AB#7953): query and formula series matched per category -->
18705
+ @if (selectedQueryFamily === 'streamData') {
18706
+ <div class="config-section">
18707
+ <h3 class="section-title">Series</h3>
18708
+ <p class="section-hint">The primary query's series come first, then the series below in this order. Bars of all series are matched by category value (e.g. the same day); each query has its own category field and can follow its own entity selector.</p>
18709
+ <mm-line-chart-series-editor
18710
+ alignment="category"
18711
+ [series]="additionalSeries"
18712
+ [selectors]="availableEntitySelectors"
18713
+ [primary]="primarySeries"
18714
+ [defaultCategoryField]="form.categoryField"
18715
+ (seriesChange)="additionalSeries = $event"
18716
+ (primaryChange)="primarySeries = $event">
18717
+ </mm-line-chart-series-editor>
18718
+ </div>
18719
+ }
18720
+
17191
18721
  <!-- Filters Section -->
17192
18722
  @if (selectedPersistentQuery?.queryCkTypeId) {
17193
18723
  <div class="config-section">
@@ -17357,8 +18887,179 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17357
18887
  type: Input
17358
18888
  }], initialFilters: [{
17359
18889
  type: Input
18890
+ }], initialAdditionalSeries: [{
18891
+ type: Input
18892
+ }], initialPrimarySeries: [{
18893
+ type: Input
17360
18894
  }] } });
17361
18895
 
18896
+ /**
18897
+ * Aligns the series of several stream-data sources on one time grid (AB#7951).
18898
+ *
18899
+ * Every source delivers its points keyed by the bucket start instant (epoch ms). Each source's
18900
+ * bucket step is its most frequent distance between consecutive instants (gaps in the data do not
18901
+ * change it). The grid step is the COARSEST source step; the source that has it (the first one on a
18902
+ * tie, i.e. the primary source when it is passed first) is the reference and gives the anchor.
18903
+ *
18904
+ * - Fixed-size grid (step up to one hour): a source is aligned when every instant is a whole number
18905
+ * of grid steps away from the reference anchor. A finer rung (5-min buckets on a 15-min grid) or
18906
+ * a shifted anchor puts instants between grid points: that source is reported as misaligned and
18907
+ * left out instead of being plotted shifted. Missing grid instants between the first and the last
18908
+ * instant are filled in, so a bucket no source has data for is still a gap.
18909
+ * - Calendar grid (longer steps: days, months, whose real length changes with DST and month
18910
+ * length): a source is aligned when none of its instants lies strictly inside a reference bucket
18911
+ * (0 < distance to the nearest reference instant < step). Instants only, no wall-clock
18912
+ * arithmetic, so days across a DST change (23 h / 25 h) align when the sources share them. No
18913
+ * fill-in.
18914
+ *
18915
+ * Gaps: a grid instant a series has no point for is `null` (rendered as a gap, never 0).
18916
+ */
18917
+ /** Largest fixed-size grid step: modular alignment check and fill-in up to here, calendar rule above. */
18918
+ const MAX_FILLED_GRID_STEP_MS = 60 * 60 * 1000;
18919
+ /** Instants a source needs for its step to count when the reference is chosen. */
18920
+ const MIN_REFERENCE_INSTANTS = 3;
18921
+ /** Upper bound of filled grid instants, guarding against a degenerate step over a long window. */
18922
+ const MAX_FILLED_GRID_POINTS = 20000;
18923
+ /** Sorted distinct instants of all series of a source. */
18924
+ function sourceInstants(source) {
18925
+ const set = new Set();
18926
+ for (const s of source.series) {
18927
+ for (const t of s.points.keys()) {
18928
+ if (Number.isFinite(t))
18929
+ set.add(t);
18930
+ }
18931
+ }
18932
+ return Array.from(set).sort((a, b) => a - b);
18933
+ }
18934
+ /**
18935
+ * Typical bucket step of sorted instants: the most frequent positive distance between consecutive
18936
+ * instants (the smaller one on a tie), `null` for fewer than two instants.
18937
+ */
18938
+ function bucketStepMs(instants) {
18939
+ const counts = new Map();
18940
+ for (let i = 1; i < instants.length; i++) {
18941
+ const d = instants[i] - instants[i - 1];
18942
+ if (d > 0)
18943
+ counts.set(d, (counts.get(d) ?? 0) + 1);
18944
+ }
18945
+ let step = null;
18946
+ let best = 0;
18947
+ for (const [d, n] of counts) {
18948
+ if (n > best || (n === best && step !== null && d < step)) {
18949
+ step = d;
18950
+ best = n;
18951
+ }
18952
+ }
18953
+ return step;
18954
+ }
18955
+ /** Distance from `t` to the nearest value in the sorted array `sorted` (Infinity when empty). */
18956
+ function distanceToNearest(sorted, t) {
18957
+ let lo = 0;
18958
+ let hi = sorted.length - 1;
18959
+ if (hi < 0)
18960
+ return Infinity;
18961
+ while (lo < hi) {
18962
+ const mid = (lo + hi) >> 1;
18963
+ if (sorted[mid] < t)
18964
+ lo = mid + 1;
18965
+ else
18966
+ hi = mid;
18967
+ }
18968
+ let best = Math.abs(sorted[lo] - t);
18969
+ if (lo > 0)
18970
+ best = Math.min(best, Math.abs(sorted[lo - 1] - t));
18971
+ return best;
18972
+ }
18973
+ /**
18974
+ * Aligns the sources on one grid. Source order is kept for the series; the first source wins a
18975
+ * tie for the coarsest step (the primary source when it is passed first).
18976
+ */
18977
+ function alignOnTimeGrid(sources) {
18978
+ const perSource = sources.map(source => {
18979
+ const instants = sourceInstants(source);
18980
+ return { source, instants, step: bucketStepMs(instants) };
18981
+ });
18982
+ // Reference = coarsest step; the first source wins a tie.
18983
+ // Reference = coarsest step; the first source wins a tie. A source with fewer than
18984
+ // MIN_REFERENCE_INSTANTS instants has no reliable step (two sparse points look like a coarse
18985
+ // rung) and is the reference only when no source has enough instants.
18986
+ const eligible = perSource.some(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
18987
+ ? perSource.filter(e => e.instants.length >= MIN_REFERENCE_INSTANTS)
18988
+ : perSource;
18989
+ let reference = null;
18990
+ for (const entry of eligible) {
18991
+ if (entry.step !== null && (reference === null || entry.step > (reference.step ?? 0))) {
18992
+ reference = entry;
18993
+ }
18994
+ }
18995
+ const gridStepMs = reference?.step ?? null;
18996
+ const misaligned = [];
18997
+ const aligned = [];
18998
+ for (const entry of perSource) {
18999
+ if (reference && gridStepMs !== null && entry !== reference) {
19000
+ const anchor = reference.instants[0];
19001
+ const off = gridStepMs <= MAX_FILLED_GRID_STEP_MS
19002
+ ? entry.instants.some(t => (t - anchor) % gridStepMs !== 0)
19003
+ : entry.instants.some(t => {
19004
+ const d = distanceToNearest(reference.instants, t);
19005
+ return d > 0 && d < gridStepMs;
19006
+ });
19007
+ if (off) {
19008
+ misaligned.push({
19009
+ sourceKey: entry.source.sourceKey,
19010
+ sourceLabel: entry.source.sourceLabel,
19011
+ stepMs: entry.step,
19012
+ gridStepMs
19013
+ });
19014
+ continue;
19015
+ }
19016
+ }
19017
+ aligned.push(entry);
19018
+ }
19019
+ const instantSet = new Set();
19020
+ for (const entry of aligned) {
19021
+ for (const t of entry.instants)
19022
+ instantSet.add(t);
19023
+ }
19024
+ let instants = Array.from(instantSet).sort((a, b) => a - b);
19025
+ if (gridStepMs !== null && gridStepMs <= MAX_FILLED_GRID_STEP_MS && instants.length > 1) {
19026
+ instants = fillGrid(instants, gridStepMs);
19027
+ }
19028
+ const series = [];
19029
+ for (const entry of aligned) {
19030
+ for (const s of entry.source.series) {
19031
+ series.push({
19032
+ name: s.name,
19033
+ sourceKey: entry.source.sourceKey,
19034
+ data: instants.map(t => s.points.get(t) ?? null),
19035
+ unit: s.unit
19036
+ });
19037
+ }
19038
+ }
19039
+ return { instants, gridStepMs, series, misaligned };
19040
+ }
19041
+ /**
19042
+ * Adds the missing multiples of `step` between consecutive instants whose distance is a whole
19043
+ * multiple of it. Distances that are not a multiple (a source-own instant inside a reference gap)
19044
+ * are left as they are.
19045
+ */
19046
+ function fillGrid(instants, step) {
19047
+ const span = instants[instants.length - 1] - instants[0];
19048
+ if (span / step > MAX_FILLED_GRID_POINTS)
19049
+ return instants;
19050
+ const out = [instants[0]];
19051
+ for (let i = 1; i < instants.length; i++) {
19052
+ const prev = instants[i - 1];
19053
+ const d = instants[i] - prev;
19054
+ if (d > step && d % step === 0) {
19055
+ for (let t = prev + step; t < instants[i]; t += step)
19056
+ out.push(t);
19057
+ }
19058
+ out.push(instants[i]);
19059
+ }
19060
+ return out;
19061
+ }
19062
+
17362
19063
  /** Series colours so a series' min/max band and its avg line share one hue. */
17363
19064
  const SERIES_PALETTE = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
17364
19065
  /**
@@ -17388,6 +19089,7 @@ class LineChartWidgetComponent {
17388
19089
  ]);
17389
19090
  stateService = inject(MeshBoardStateService);
17390
19091
  variableService = inject(MeshBoardVariableService);
19092
+ expressionEvaluator = inject(ExpressionEvaluatorService);
17391
19093
  config;
17392
19094
  // Widget state signals
17393
19095
  _isLoading = signal(false, /* @ts-ignore */
@@ -17425,6 +19127,10 @@ class LineChartWidgetComponent {
17425
19127
  valueAxes = this._valueAxes.asReadonly();
17426
19128
  error = this._error.asReadonly();
17427
19129
  dataInfo = this._dataInfo.asReadonly();
19130
+ // Per-source hints of a multi-source chart (AB#7951); always empty for a single-query chart.
19131
+ _sourceHints = signal([], /* @ts-ignore */
19132
+ ...(ngDevMode ? [{ debugName: "_sourceHints" }] : /* istanbul ignore next */ []));
19133
+ sourceHints = this._sourceHints.asReadonly();
17428
19134
  /**
17429
19135
  * Resolution-aware hint (AB#4290): a short badge describing the archive-selection outcome —
17430
19136
  * `null` for a clean reduction (signal OK), a warning otherwise (fewer points delivered, or no
@@ -17567,6 +19273,49 @@ class LineChartWidgetComponent {
17567
19273
  return this.config?.chartType ?? 'line';
17568
19274
  }, /* @ts-ignore */
17569
19275
  ...(ngDevMode ? [{ debugName: "chartType" }] : /* istanbul ignore next */ []));
19276
+ /**
19277
+ * Stacking (AB#7951): only an area chart with `stacked` stacks; the series stack in their order
19278
+ * (primary series first, then `additionalSeries`). `false` is Kendo's default, so an existing
19279
+ * chart stacks exactly as before (not at all).
19280
+ */
19281
+ stackSeries() {
19282
+ return !!this.config?.stacked && this.chartType() === 'area';
19283
+ }
19284
+ /**
19285
+ * Kendo `missingValues` (AB#7951), always set explicitly. A stacked or multi-source chart uses
19286
+ * `'gap'`: a bucket without data is a gap, never drawn or stacked as 0 and never bridged.
19287
+ * Single-query unstacked charts get exactly Kendo's defaults — `'interpolate'` for a line,
19288
+ * `'zero'` for an area series — so they render as before.
19289
+ */
19290
+ missingValues() {
19291
+ if (this.stackSeries() || this.hasAdditionalSeries())
19292
+ return 'gap';
19293
+ return this.chartType() === 'area' ? 'zero' : 'interpolate';
19294
+ }
19295
+ /** Whether the chart reads series from more than its primary query (AB#7951) or has formula series (AB#7952). */
19296
+ hasAdditionalSeries() {
19297
+ return (this.config?.additionalSeries ?? []).some(s => isCompleteQuerySeries(s) || isCompleteFormulaSeries(s));
19298
+ }
19299
+ /** Whether the last load went through the multi-source path (aligned grid, AB#7951/AB#7952). */
19300
+ _multiSource = signal(false, /* @ts-ignore */
19301
+ ...(ngDevMode ? [{ debugName: "_multiSource" }] : /* istanbul ignore next */ []));
19302
+ isMultiSource = this._multiSource.asReadonly();
19303
+ /**
19304
+ * Shared tooltip rows of the multi-source path (AB#7952): one row per drawn series for the
19305
+ * hovered category; a gap (null) shows "–".
19306
+ */
19307
+ tooltipRows(category) {
19308
+ const index = this._categories().indexOf(category);
19309
+ if (index < 0)
19310
+ return [];
19311
+ return this._seriesData().map(s => {
19312
+ const value = s.data[index];
19313
+ return {
19314
+ name: s.name,
19315
+ text: value === null || value === undefined ? '–' : `${this.formatValue(value)}${this.getUnitForSeries(s.name)}`
19316
+ };
19317
+ });
19318
+ }
17570
19319
  labelRotation = computed(() => {
17571
19320
  const categoryCount = this._categories().length;
17572
19321
  return categoryCount > 5 ? -45 : 0;
@@ -17668,6 +19417,12 @@ class LineChartWidgetComponent {
17668
19417
  async loadData() {
17669
19418
  if (this.isNotConfigured())
17670
19419
  return;
19420
+ this._multiSource.set(this.hasAdditionalSeries());
19421
+ if (this.hasAdditionalSeries()) {
19422
+ await this.loadMultiSource();
19423
+ return;
19424
+ }
19425
+ this._sourceHints.set([]);
17671
19426
  const queryDataSource = this.config.dataSource;
17672
19427
  this._isLoading.set(true);
17673
19428
  this._error.set(null);
@@ -17849,6 +19604,240 @@ class LineChartWidgetComponent {
17849
19604
  ]
17850
19605
  };
17851
19606
  }
19607
+ /**
19608
+ * Multi-source load (AB#7951): the primary data source plus every complete query series, each
19609
+ * with its own scope, time-filter opt-out and resolution-aware routing, aligned on one time grid.
19610
+ *
19611
+ * 1. Scope: any source pending → stay loading, no query; an empty scope → the source has no
19612
+ * series and a "No data for <source>" hint.
19613
+ * 2. Prepare: resolution-aware sources resolve their rollup rung.
19614
+ * 3. Common bucket count: the smallest of the pixel-derived target and every resolved
19615
+ * `points` (= the coarsest effective bucket). Every source is downsampled with this count over
19616
+ * the same window, so all of them use the same bucket width.
19617
+ * 4. Fetch all sources in parallel; one failing source gives an error hint, the others render.
19618
+ * 5. {@link alignOnTimeGrid}: shared categories, gaps as null, misaligned sources left out with a hint.
19619
+ */
19620
+ async loadMultiSource() {
19621
+ const primaryDs = this.config.dataSource;
19622
+ const sources = [
19623
+ {
19624
+ key: PRIMARY_SOURCE_KEY,
19625
+ label: primaryDs.queryName?.trim() || 'Primary query',
19626
+ dataSource: primaryDs,
19627
+ valueField: this.config.valueField,
19628
+ primary: true
19629
+ },
19630
+ ...(this.config.additionalSeries ?? [])
19631
+ .filter(isCompleteQuerySeries)
19632
+ .map((def) => ({
19633
+ key: def.key,
19634
+ label: querySeriesLabel(def),
19635
+ dataSource: def.dataSource,
19636
+ valueField: def.valueField,
19637
+ primary: false
19638
+ }))
19639
+ ];
19640
+ this._isLoading.set(true);
19641
+ this._error.set(null);
19642
+ const scopes = sources.map(s => this.stateService.resolveStreamDataScope(s.dataSource.entitySelectorId, s.dataSource.queryFamily, s.dataSource.ignoreTimeFilter));
19643
+ if (scopes.some(s => s.kind === 'pending')) {
19644
+ return;
19645
+ }
19646
+ try {
19647
+ const hints = [];
19648
+ const problems = new Map();
19649
+ const active = [];
19650
+ sources.forEach((source, i) => {
19651
+ if (scopes[i].kind === 'empty') {
19652
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
19653
+ problems.set(source.key, { label: source.label, problem: 'noData' });
19654
+ }
19655
+ else {
19656
+ active.push(source);
19657
+ }
19658
+ });
19659
+ const prepared = await Promise.allSettled(active.map(s => this.prepareSource(s)));
19660
+ let limit = this.computeDownsampleLimit();
19661
+ const targetPoints = limit;
19662
+ let signal = null;
19663
+ for (const p of prepared) {
19664
+ const resolution = p.status === 'fulfilled' ? p.value.resolutionAware?.resolution : null;
19665
+ if (resolution) {
19666
+ limit = Math.min(limit, Math.max(1, resolution.points));
19667
+ signal ??= resolution;
19668
+ }
19669
+ }
19670
+ const fetched = await Promise.allSettled(active.map((source, i) => {
19671
+ const p = prepared[i];
19672
+ return p.status === 'fulfilled'
19673
+ ? this.fetchSource(source, p.value, limit)
19674
+ : Promise.reject(p.reason);
19675
+ }));
19676
+ const gridSources = [];
19677
+ let rowCount = 0;
19678
+ fetched.forEach((f, i) => {
19679
+ const source = active[i];
19680
+ if (f.status === 'rejected') {
19681
+ console.error(`Error loading Line Chart source "${source.label}":`, f.reason);
19682
+ hints.push({ kind: 'error', sourceLabel: source.label, text: `${source.label}: ${this.errorMessage(f.reason)}` });
19683
+ problems.set(source.key, { label: source.label, problem: 'error' });
19684
+ return;
19685
+ }
19686
+ rowCount += f.value.rows.length;
19687
+ const series = source.primary
19688
+ ? primaryRowsToSeries(f.value.rows, this.config, f.value.downsampled)
19689
+ : [queryRowsToSeries(f.value.rows, source.label, source.valueField, f.value.downsampled)];
19690
+ const withData = series.filter(s => s.points.size > 0);
19691
+ if (withData.length === 0) {
19692
+ hints.push({ kind: 'noData', sourceLabel: source.label, text: `No data for ${source.label}` });
19693
+ problems.set(source.key, { label: source.label, problem: 'noData' });
19694
+ return;
19695
+ }
19696
+ gridSources.push({ sourceKey: source.key, sourceLabel: source.label, series: withData });
19697
+ });
19698
+ const grid = alignOnTimeGrid(gridSources);
19699
+ for (const m of grid.misaligned) {
19700
+ hints.push({
19701
+ kind: 'misaligned',
19702
+ sourceLabel: m.sourceLabel,
19703
+ text: `${m.sourceLabel}: its buckets do not match the chart's time grid and are not shown`
19704
+ });
19705
+ problems.set(m.sourceKey, { label: m.sourceLabel, problem: 'misaligned' });
19706
+ }
19707
+ // Formula series, per-series visibility and stacking (AB#7952).
19708
+ const composed = composeChartSeries({
19709
+ gridSeries: grid.series,
19710
+ length: grid.instants.length,
19711
+ definitions: this.config.additionalSeries ?? [],
19712
+ primary: this.config.primarySeries,
19713
+ stackedChart: this.stackSeries(),
19714
+ sourceProblems: problems,
19715
+ evaluator: this.expressionEvaluator
19716
+ });
19717
+ hints.push(...composed.hints);
19718
+ this.applyAlignedGrid(grid.instants, composed.series);
19719
+ this._sourceHints.set(hints);
19720
+ this._resolutionSignal.set(signal);
19721
+ this._resolutionTarget.set(signal ? targetPoints : null);
19722
+ this.lastLimit = limit;
19723
+ this._dataInfo.set({ rows: rowCount, points: grid.instants.length, total: rowCount });
19724
+ this._isLoading.set(false);
19725
+ }
19726
+ catch (err) {
19727
+ console.error('Error loading Line Chart data:', err);
19728
+ this._error.set('Failed to load data');
19729
+ this._dataInfo.set(null);
19730
+ this._resolutionSignal.set(null);
19731
+ this._sourceHints.set([]);
19732
+ this._isLoading.set(false);
19733
+ }
19734
+ }
19735
+ /** Resolution-aware sources resolve their rung first (step 2 of {@link loadMultiSource}). */
19736
+ async prepareSource(source) {
19737
+ const ds = source.dataSource;
19738
+ const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
19739
+ const rtIds = this.stateService.resolveStreamDataRtIds(ds.entitySelectorId);
19740
+ if (!(ds.resolutionAware && ds.queryFamily === 'streamData' && source.valueField && timeArgs?.from && timeArgs?.to)) {
19741
+ return { timeArgs, rtIds };
19742
+ }
19743
+ const info = await this.queryExecutor.fetchQueryArchive(ds.queryRtId);
19744
+ if (!info) {
19745
+ throw new Error('base archive not found for the selected query');
19746
+ }
19747
+ const scopeRtIds = rtIds && rtIds.length > 0 ? rtIds : (info.rtIds ?? undefined);
19748
+ const resolution = await this.queryExecutor.resolveSeriesQuery({
19749
+ baseArchiveRtId: info.archiveRtId,
19750
+ from: timeArgs.from,
19751
+ to: timeArgs.to,
19752
+ targetPoints: this.computeDownsampleLimit(),
19753
+ requiredAggregation: (ds.requiredAggregation ?? 'SUM'),
19754
+ sourcePath: source.valueField,
19755
+ rtIds: scopeRtIds,
19756
+ timeZone: this.stateService.resolveStreamDataTimeZone()
19757
+ });
19758
+ return {
19759
+ timeArgs,
19760
+ rtIds,
19761
+ resolutionAware: { resolution, ckTypeId: info.ckTypeId, scopeRtIds, from: timeArgs.from, to: timeArgs.to }
19762
+ };
19763
+ }
19764
+ /** Fetches one source with the common bucket count (step 4 of {@link loadMultiSource}). */
19765
+ async fetchSource(source, prepared, limit) {
19766
+ const ds = source.dataSource;
19767
+ // Widget filters belong to the primary query's type; query series run unfiltered.
19768
+ const fieldFilter = source.primary ? this.convertFiltersToDto(this.config.filters) ?? undefined : undefined;
19769
+ const ra = prepared.resolutionAware;
19770
+ if (ra) {
19771
+ const resolution = ra.resolution;
19772
+ if (!resolution)
19773
+ return { rows: [], downsampled: true };
19774
+ const downsample = (rtIds, label) => this.queryExecutor.downsampleByArchive({
19775
+ archiveRtId: resolution.archiveRtId,
19776
+ from: ra.from,
19777
+ to: ra.to,
19778
+ limit,
19779
+ sourcePath: source.valueField,
19780
+ aggregation: resolution.reducingFunction,
19781
+ rtIds: rtIds && rtIds.length > 0 ? rtIds : undefined,
19782
+ fieldFilter
19783
+ }).then(rows => source.primary
19784
+ ? rows.map(r => this.reshapeResolutionRow(r, label))
19785
+ // The transient row's one value column is named after the reducer (`<field>_min` for MIN …);
19786
+ // re-emit it under the series' value field so no reducer suffix is mistaken for a band.
19787
+ : rows.map(r => ({ ...r, cells: [{ attributePath: source.valueField, value: r.cells.find(c => c.attributePath.toLowerCase() !== 'timestamp')?.value ?? null }] })));
19788
+ if (!source.primary) {
19789
+ // One source = one series: a single call over the whole scope, reduced server-side.
19790
+ return { rows: await downsample(ra.scopeRtIds, source.label), downsampled: true };
19791
+ }
19792
+ const rows = ds.aggregateSeriesByGroup && ra.ckTypeId
19793
+ ? await this.loadGroupAggregatedRows(ra.ckTypeId, ra.scopeRtIds, downsample)
19794
+ : await this.loadPerRtIdRows(ra.ckTypeId, ra.scopeRtIds, downsample);
19795
+ return { rows, downsampled: true };
19796
+ }
19797
+ const scope = this.stateService.resolveStreamDataScope(ds.entitySelectorId, ds.queryFamily, ds.ignoreTimeFilter);
19798
+ const timeArgs = prepared.timeArgs;
19799
+ const downsampled = ds.queryFamily === 'streamData' && !!timeArgs?.from && !!timeArgs?.to;
19800
+ const streamDataArgs = downsampled
19801
+ ? { ...timeArgs, rtIds: prepared.rtIds, limit, queryMode: QueryModeDto.DownsamplingDto }
19802
+ : (timeArgs || prepared.rtIds ? { ...timeArgs, rtIds: prepared.rtIds } : undefined);
19803
+ const result = await firstValueFrom(this.queryExecutor.execute(ds.queryFamily, ds.queryRtId, {
19804
+ fieldFilter,
19805
+ streamDataArgs,
19806
+ scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
19807
+ }));
19808
+ const rows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
19809
+ return { rows, downsampled };
19810
+ }
19811
+ /** Writes aligned series into the chart signals (multi-source path, AB#7951). */
19812
+ applyAlignedGrid(instants, series) {
19813
+ const mode = this.stateService.timeZoneMode();
19814
+ const days = new Set(instants.map(t => formatInstant(new Date(t), mode, { day: '2-digit', month: '2-digit', year: 'numeric' }) ?? '?'));
19815
+ const needsTime = days.size < instants.length;
19816
+ const categories = uniqueCategoryLabels(instants, d => needsTime ? this.formatDateTime(d) : this.formatDate(d));
19817
+ const unitMap = new Map();
19818
+ const seriesData = series.map((s, index) => {
19819
+ if (s.unit)
19820
+ unitMap.set(s.name, s.unit);
19821
+ return {
19822
+ name: s.name,
19823
+ data: s.data,
19824
+ unit: s.unit,
19825
+ color: SERIES_PALETTE[index % SERIES_PALETTE.length],
19826
+ stacked: s.stacked,
19827
+ asLine: s.asLine
19828
+ };
19829
+ });
19830
+ this._categories.set(categories);
19831
+ this._seriesData.set(seriesData);
19832
+ // One value axis: series of different queries share it (per-unit axes stay a single-query feature).
19833
+ this._valueAxes.set([]);
19834
+ this._seriesUnitMap.set(unitMap);
19835
+ }
19836
+ /** Short message of a failed source for its hint. */
19837
+ errorMessage(reason) {
19838
+ const message = reason instanceof Error ? reason.message : typeof reason === 'string' ? reason : '';
19839
+ return message ? `Failed to load data (${message})` : 'Failed to load data';
19840
+ }
17852
19841
  buildStreamDataArgs() {
17853
19842
  const ds = this.config.dataSource;
17854
19843
  const timeArgs = this.stateService.resolveStreamDataTimeArgs(ds.ignoreTimeFilter);
@@ -17918,10 +19907,17 @@ class LineChartWidgetComponent {
17918
19907
  lineValue = this.toNumber(cell.value); // _avg (or an unsuffixed value)
17919
19908
  }
17920
19909
  else {
17921
- const val = cell.value;
17922
- lineValue = typeof val === 'number' ? val : parseFloat(String(val));
17923
- if (isNaN(lineValue))
17924
- 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
+ }
17925
19921
  }
17926
19922
  }
17927
19923
  else if (unitField && matchesAttributePath(path, unitField)) {
@@ -18061,6 +20057,15 @@ class LineChartWidgetComponent {
18061
20057
  </span>
18062
20058
  }
18063
20059
  }
20060
+ @if (sourceHints().length > 0) {
20061
+ <ul class="source-hints" aria-live="polite">
20062
+ @for (hint of sourceHints(); track hint.text) {
20063
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
20064
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
20065
+ </li>
20066
+ }
20067
+ </ul>
20068
+ }
18064
20069
  @if (showResolutionInfo() && resolutionExplanation(); as msg) {
18065
20070
  <div class="resolution-info">
18066
20071
  <div class="ri-head">
@@ -18132,13 +20137,15 @@ class LineChartWidgetComponent {
18132
20137
  </kendo-chart-series-item>
18133
20138
  }
18134
20139
  <kendo-chart-series-item
18135
- [type]="chartType()"
20140
+ [type]="series.asLine ? 'line' : chartType()"
18136
20141
  [style]="'smooth'"
18137
20142
  [data]="series.data"
18138
20143
  [name]="series.name"
18139
20144
  [axis]="series.axisName ?? ''"
18140
20145
  [color]="series.color"
18141
- [opacity]="0.7"
20146
+ [opacity]="series.asLine ? 1 : 0.7"
20147
+ [stack]="series.stacked ?? stackSeries()"
20148
+ [missingValues]="missingValues()"
18142
20149
  [markers]="{ visible: config.showMarkers ?? false }">
18143
20150
  </kendo-chart-series-item>
18144
20151
  }
@@ -18161,9 +20168,16 @@ class LineChartWidgetComponent {
18161
20168
  <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
18162
20169
  <div class="chart-tooltip">
18163
20170
  <strong>{{ category }}</strong>
18164
- @for (point of points; track point.series.name) {
18165
- @if (!isBandSeries(point.series.name)) {
18166
- <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
+ }
18167
20181
  }
18168
20182
  }
18169
20183
  </div>
@@ -18172,7 +20186,7 @@ class LineChartWidgetComponent {
18172
20186
  </kendo-chart>
18173
20187
  }
18174
20188
  </div>
18175
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
20189
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ChartsModule }, { kind: "component", type: i1$7.ChartComponent, selector: "kendo-chart", inputs: ["pannable", "renderAs", "seriesColors", "subtitle", "title", "noData", "observeStyles", "transitions", "zoomable", "axisDefaults", "categoryAxis", "chartArea", "legend", "panes", "paneDefaults", "plotArea", "series", "seriesDefaults", "tooltip", "valueAxis", "xAxis", "yAxis", "resizeRateLimit", "popupSettings", "drilldownLevel"], outputs: ["axisLabelClick", "drag", "dragEnd", "dragStart", "legendItemHover", "legendItemLeave", "noteClick", "noteHover", "noteLeave", "paneRender", "plotAreaClick", "plotAreaHover", "plotAreaLeave", "render", "select", "selectEnd", "selectStart", "seriesClick", "drilldown", "seriesHover", "seriesOver", "seriesLeave", "zoom", "zoomEnd", "zoomStart", "legendItemClick", "drilldownLevelChange"], exportAs: ["kendoChart"] }, { kind: "directive", type: i1$7.SharedTooltipTemplateDirective, selector: "[kendoChartSharedTooltipTemplate]" }, { kind: "component", type: i1$7.CategoryAxisComponent, selector: "kendo-chart-category-axis" }, { kind: "component", type: i1$7.CategoryAxisItemComponent, selector: "kendo-chart-category-axis-item", inputs: ["autoBaseUnitSteps", "axisCrossingValue", "background", "baseUnit", "baseUnitStep", "categories", "color", "justified", "line", "majorGridLines", "majorTicks", "max", "maxDateGroups", "maxDivisions", "min", "minorGridLines", "minorTicks", "name", "pane", "plotBands", "reverse", "roundToBaseUnit", "startAngle", "type", "visible", "weekStartDay", "crosshair", "labels", "notes", "select", "title", "rangeLabels", "highlight"] }, { kind: "component", type: i1$7.CategoryAxisLabelsComponent, selector: "kendo-chart-category-axis-item-labels", inputs: ["background", "border", "color", "content", "culture", "dateFormats", "font", "format", "margin", "mirror", "padding", "position", "rotation", "skip", "step", "visible", "visual"] }, { kind: "component", type: i1$7.ChartAreaComponent, selector: "kendo-chart-area", inputs: ["background", "border", "height", "margin", "opacity", "width"] }, { kind: "component", type: i1$7.LegendComponent, selector: "kendo-chart-legend", inputs: ["align", "background", "border", "height", "labels", "margin", "offsetX", "offsetY", "orientation", "padding", "position", "reverse", "visible", "width", "markers", "spacing", "inactiveItems", "item", "title", "focusHighlight"] }, { kind: "component", type: i1$7.SeriesComponent, selector: "kendo-chart-series" }, { kind: "component", type: i1$7.SeriesItemComponent, selector: "kendo-chart-series-item", inputs: ["aggregate", "autoFit", "axis", "border", "categoryAxis", "categoryField", "closeField", "color", "colorField", "connectors", "currentField", "dashType", "data", "downColor", "downColorField", "drilldownField", "dynamicHeight", "dynamicSlope", "errorHighField", "errorLowField", "explodeField", "field", "fromField", "gap", "highField", "holeSize", "line", "lowField", "lowerField", "margin", "maxSize", "mean", "meanField", "median", "medianField", "minSize", "missingValues", "name", "neckRatio", "negativeColor", "negativeValues", "noteTextField", "opacity", "openField", "outliersField", "overlay", "padding", "q1Field", "q3Field", "segmentSpacing", "size", "sizeField", "spacing", "stack", "startAngle", "lineStyle", "summaryField", "target", "toField", "type", "upperField", "visible", "visibleInLegend", "visibleInLegendField", "visual", "width", "whiskers", "xAxis", "xErrorHighField", "xErrorLowField", "xField", "yAxis", "yErrorHighField", "yErrorLowField", "yField", "zIndex", "trendline", "for", "legendItem", "pattern", "patternField", "errorBars", "extremes", "highlight", "labels", "markers", "notes", "outliers", "tooltip"] }, { kind: "component", type: i1$7.TooltipComponent, selector: "kendo-chart-tooltip", inputs: ["background", "border", "color", "font", "format", "opacity", "padding", "shared", "visible"] }, { kind: "component", type: i1$7.ValueAxisComponent, selector: "kendo-chart-value-axis" }, { kind: "component", type: i1$7.ValueAxisItemComponent, selector: "kendo-chart-value-axis-item", inputs: ["axisCrossingValue", "background", "color", "line", "majorGridLines", "majorTicks", "majorUnit", "max", "min", "minorGridLines", "minorTicks", "minorUnit", "name", "narrowRange", "pane", "plotBands", "reverse", "type", "visible", "crosshair", "labels", "notes", "title"] }, { kind: "component", type: WidgetNotConfiguredComponent, selector: "mm-widget-not-configured" }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18176
20190
  }
18177
20191
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
18178
20192
  type: Component,
@@ -18202,6 +20216,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18202
20216
  </span>
18203
20217
  }
18204
20218
  }
20219
+ @if (sourceHints().length > 0) {
20220
+ <ul class="source-hints" aria-live="polite">
20221
+ @for (hint of sourceHints(); track hint.text) {
20222
+ <li class="source-hint" [class.error]="hint.kind === 'error'" [class.warn]="hint.kind === 'misaligned' || hint.kind === 'dependency'">
20223
+ @if (hint.kind !== 'noData') {<span aria-hidden="true">⚠ </span>}{{ hint.text }}
20224
+ </li>
20225
+ }
20226
+ </ul>
20227
+ }
18205
20228
  @if (showResolutionInfo() && resolutionExplanation(); as msg) {
18206
20229
  <div class="resolution-info">
18207
20230
  <div class="ri-head">
@@ -18273,13 +20296,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18273
20296
  </kendo-chart-series-item>
18274
20297
  }
18275
20298
  <kendo-chart-series-item
18276
- [type]="chartType()"
20299
+ [type]="series.asLine ? 'line' : chartType()"
18277
20300
  [style]="'smooth'"
18278
20301
  [data]="series.data"
18279
20302
  [name]="series.name"
18280
20303
  [axis]="series.axisName ?? ''"
18281
20304
  [color]="series.color"
18282
- [opacity]="0.7"
20305
+ [opacity]="series.asLine ? 1 : 0.7"
20306
+ [stack]="series.stacked ?? stackSeries()"
20307
+ [missingValues]="missingValues()"
18283
20308
  [markers]="{ visible: config.showMarkers ?? false }">
18284
20309
  </kendo-chart-series-item>
18285
20310
  }
@@ -18302,9 +20327,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18302
20327
  <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
18303
20328
  <div class="chart-tooltip">
18304
20329
  <strong>{{ category }}</strong>
18305
- @for (point of points; track point.series.name) {
18306
- @if (!isBandSeries(point.series.name)) {
18307
- <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
+ }
18308
20340
  }
18309
20341
  }
18310
20342
  </div>
@@ -18313,7 +20345,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18313
20345
  </kendo-chart>
18314
20346
  }
18315
20347
  </div>
18316
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
20348
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.line-chart-widget{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}.data-count{position:absolute;top:4px;right:6px;z-index:2;max-width:calc(100% - 12px);padding:1px 7px;border-radius:9px;font-size:.7rem;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--kendo-color-on-app-surface, #6c757d);background:color-mix(in srgb,var(--kendo-color-app-surface, #888) 12%,transparent);pointer-events:none;-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.data-count.warn{color:var(--kendo-color-warning, #b8860b);background:color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 16%,transparent)}.data-count.has-info{pointer-events:auto;cursor:pointer}.data-count .rl-flag{font-weight:600}.resolution-info{position:absolute;top:26px;right:6px;z-index:5;max-width:min(340px,calc(100% - 12px));padding:8px 10px;border-radius:6px;font-size:.75rem;line-height:1.4;color:var(--kendo-color-on-app-surface, #212529);background:var(--kendo-color-app-surface, #fff);border:1px solid color-mix(in srgb,var(--kendo-color-warning, #f0ad4e) 55%,transparent);box-shadow:0 4px 14px #0000002e;text-align:left}.resolution-info .ri-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-weight:600;color:var(--kendo-color-warning, #b8860b)}.resolution-info .ri-close{border:none;background:transparent;cursor:pointer;font-size:.8rem;line-height:1;color:var(--kendo-color-subtle, #6c757d);padding:0 2px}.resolution-info .ri-body{margin:0}.source-hints{position:absolute;top:4px;left:6px;z-index:2;max-width:calc(100% - 140px);margin:0;padding:0;list-style:none;font-size:.7rem;line-height:1.4;color:var(--kendo-color-subtle, #6c757d)}.source-hint{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-hint.error{color:var(--kendo-color-error, #dc3545)}.source-hint.warn{color:var(--kendo-color-warning, #b8860b)}.line-chart-widget.loading,.line-chart-widget.error{opacity:.7}.loading-indicator,.error-message{display:flex;align-items:center;justify-content:center;height:100%;width:100%}.loading-indicator span{font-size:1.5rem;color:var(--kendo-color-subtle, #6c757d)}.error-message span{color:var(--kendo-color-error, #dc3545);font-size:.875rem}.chart-container{width:100%;height:100%}kendo-chart{width:100%;height:100%}.chart-tooltip{padding:4px 8px}\n"] }]
18317
20349
  }], propDecorators: { config: [{
18318
20350
  type: Input
18319
20351
  }] } });
@@ -18327,6 +20359,7 @@ class LineChartConfigDialogComponent {
18327
20359
  queryExecutor = inject(QueryExecutorService);
18328
20360
  stateService = inject(MeshBoardStateService);
18329
20361
  windowRef = inject(WindowRef);
20362
+ expressionEvaluator = inject(ExpressionEvaluatorService);
18330
20363
  querySelector;
18331
20364
  // Initial values for editing
18332
20365
  initialQueryRtId;
@@ -18347,6 +20380,13 @@ class LineChartConfigDialogComponent {
18347
20380
  initialShowDataBadge;
18348
20381
  initialReferenceLines;
18349
20382
  initialFilters;
20383
+ initialAdditionalSeries;
20384
+ initialStacked;
20385
+ initialPrimarySeries;
20386
+ /** Query and formula series of the Series section (AB#7951, AB#7952). */
20387
+ additionalSeries = [];
20388
+ /** Display options of the primary query's series (AB#7952). */
20389
+ primarySeries;
18350
20390
  // State
18351
20391
  isLoadingInitial = false;
18352
20392
  isLoadingColumns = false;
@@ -18401,13 +20441,19 @@ class LineChartConfigDialogComponent {
18401
20441
  legendPosition: 'right',
18402
20442
  showMarkers: false,
18403
20443
  showDataBadge: true,
20444
+ stacked: false,
18404
20445
  referenceLines: []
18405
20446
  };
18406
20447
  get isValid() {
18407
20448
  return this.selectedPersistentQuery !== null
18408
20449
  && this.form.categoryField !== ''
18409
20450
  && this.form.seriesGroupField !== ''
18410
- && this.form.valueField !== '';
20451
+ && this.form.valueField !== ''
20452
+ && querySeriesErrors(this.effectiveAdditionalSeries, this.expressionEvaluator).every(e => e === null);
20453
+ }
20454
+ /** The query series that are saved: none unless the primary query is a stream-data query. */
20455
+ get effectiveAdditionalSeries() {
20456
+ return this.selectedQueryFamily === 'streamData' ? this.additionalSeries : [];
18411
20457
  }
18412
20458
  async ngOnInit() {
18413
20459
  // Initialize filter variables from MeshBoard state
@@ -18431,6 +20477,9 @@ class LineChartConfigDialogComponent {
18431
20477
  this.form.showMarkers = this.initialShowMarkers ?? false;
18432
20478
  this.form.showDataBadge = this.initialShowDataBadge ?? true;
18433
20479
  this.form.referenceLines = this.initialReferenceLines ? [...this.initialReferenceLines] : [];
20480
+ this.form.stacked = this.initialStacked ?? false;
20481
+ this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
20482
+ this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
18434
20483
  // Initialize filters
18435
20484
  if (this.initialFilters && this.initialFilters.length > 0) {
18436
20485
  this.filters = this.initialFilters.map((f, index) => ({
@@ -18512,8 +20561,10 @@ class LineChartConfigDialogComponent {
18512
20561
  aggregationType: c.aggregationType ?? null
18513
20562
  }));
18514
20563
  }
18515
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
18516
- 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 => ({
18517
20568
  attributePath: c.attributePath,
18518
20569
  attributeValueType: c.attributeValueType ?? '',
18519
20570
  aggregationType: c.aggregationType ?? null
@@ -18553,7 +20604,10 @@ class LineChartConfigDialogComponent {
18553
20604
  showMarkers: this.form.showMarkers,
18554
20605
  showDataBadge: this.form.showDataBadge,
18555
20606
  referenceLines: this.form.referenceLines.length > 0 ? this.form.referenceLines : undefined,
18556
- filters: filtersDto
20607
+ filters: filtersDto,
20608
+ additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
20609
+ stacked: this.form.chartType === 'area' && this.form.stacked ? true : undefined,
20610
+ primarySeries: this.selectedQueryFamily === 'streamData' ? this.primarySeries : undefined
18557
20611
  };
18558
20612
  this.windowRef.close(result);
18559
20613
  }
@@ -18567,7 +20621,7 @@ class LineChartConfigDialogComponent {
18567
20621
  this.windowRef.close();
18568
20622
  }
18569
20623
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18570
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
20624
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: LineChartConfigDialogComponent, isStandalone: true, selector: "mm-line-chart-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialResolutionAware: "initialResolutionAware", initialRequiredAggregation: "initialRequiredAggregation", initialChartType: "initialChartType", initialCategoryField: "initialCategoryField", initialSeriesGroupField: "initialSeriesGroupField", initialValueField: "initialValueField", initialUnitField: "initialUnitField", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialShowMarkers: "initialShowMarkers", initialShowDataBadge: "initialShowDataBadge", initialReferenceLines: "initialReferenceLines", initialFilters: "initialFilters", initialAdditionalSeries: "initialAdditionalSeries", initialStacked: "initialStacked", initialPrimarySeries: "initialPrimarySeries" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
18571
20625
  <div class="config-container">
18572
20626
 
18573
20627
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -18708,6 +20762,22 @@ class LineChartConfigDialogComponent {
18708
20762
  </div>
18709
20763
  }
18710
20764
 
20765
+ <!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
20766
+ @if (selectedQueryFamily === 'streamData') {
20767
+ <div class="config-section">
20768
+ <h3 class="section-title">Series</h3>
20769
+ <p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
20770
+ <mm-line-chart-series-editor
20771
+ [series]="additionalSeries"
20772
+ [selectors]="availableEntitySelectors"
20773
+ [primary]="primarySeries"
20774
+ [stackedChart]="form.chartType === 'area' && form.stacked"
20775
+ (seriesChange)="additionalSeries = $event"
20776
+ (primaryChange)="primarySeries = $event">
20777
+ </mm-line-chart-series-editor>
20778
+ </div>
20779
+ }
20780
+
18711
20781
  <!-- Filters Section -->
18712
20782
  @if (selectedPersistentQuery?.queryCkTypeId) {
18713
20783
  <div class="config-section">
@@ -18749,6 +20819,16 @@ class LineChartConfigDialogComponent {
18749
20819
  </div>
18750
20820
  </div>
18751
20821
 
20822
+ @if (form.chartType === 'area') {
20823
+ <div class="form-field checkbox-field">
20824
+ <label>
20825
+ <input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
20826
+ Stacked
20827
+ </label>
20828
+ </div>
20829
+ <p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
20830
+ }
20831
+
18752
20832
  <div class="form-row">
18753
20833
  <div class="form-field checkbox-field">
18754
20834
  <label>
@@ -18812,7 +20892,7 @@ class LineChartConfigDialogComponent {
18812
20892
  </button>
18813
20893
  </div>
18814
20894
  </div>
18815
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
20895
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.config-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.section-title{margin:0 0 16px;font-size:1rem;font-weight:600;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.form-field:last-child{margin-bottom:0}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.required{color:var(--kendo-color-error, #dc3545)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.chart-type-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.radio-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.form-row{display:flex;gap:24px;align-items:center}.checkbox-field{flex-direction:row;align-items:center;margin-bottom:0}.checkbox-field label{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:400}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{margin-top:8px}.form-section h4{margin:0 0 4px;font-size:.95rem;font-weight:600}.reference-line-row{display:flex;gap:8px;align-items:center;margin-bottom:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.RadioControlValueAccessor, selector: "input[type=radio]:not([ngNoCva])[formControlName],input[type=radio]:not([ngNoCva])[formControl],input[type=radio]:not([ngNoCva])[ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i3.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i3.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }, { kind: "component", type: LineChartSeriesEditorComponent, selector: "mm-line-chart-series-editor", inputs: ["series", "primary", "stackedChart", "selectors", "alignment", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
18816
20896
  }
18817
20897
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
18818
20898
  type: Component,
@@ -18827,7 +20907,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18827
20907
  QuerySelectorComponent,
18828
20908
  SdTimeFilterToggleComponent,
18829
20909
  EntitySelectorScopePickerComponent,
18830
- LoadingOverlayComponent
20910
+ LoadingOverlayComponent,
20911
+ LineChartSeriesEditorComponent
18831
20912
  ], template: `
18832
20913
  <div class="config-container">
18833
20914
 
@@ -18969,6 +21050,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
18969
21050
  </div>
18970
21051
  }
18971
21052
 
21053
+ <!-- Series Section (AB#7951): query series from other stream-data queries on one time grid -->
21054
+ @if (selectedQueryFamily === 'streamData') {
21055
+ <div class="config-section">
21056
+ <h3 class="section-title">Series</h3>
21057
+ <p class="section-hint">The primary query's series come first, then the series below in this order (in a stacked area chart: bottom to top). All series share one time axis; each query can follow its own entity selector.</p>
21058
+ <mm-line-chart-series-editor
21059
+ [series]="additionalSeries"
21060
+ [selectors]="availableEntitySelectors"
21061
+ [primary]="primarySeries"
21062
+ [stackedChart]="form.chartType === 'area' && form.stacked"
21063
+ (seriesChange)="additionalSeries = $event"
21064
+ (primaryChange)="primarySeries = $event">
21065
+ </mm-line-chart-series-editor>
21066
+ </div>
21067
+ }
21068
+
18972
21069
  <!-- Filters Section -->
18973
21070
  @if (selectedPersistentQuery?.queryCkTypeId) {
18974
21071
  <div class="config-section">
@@ -19010,6 +21107,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19010
21107
  </div>
19011
21108
  </div>
19012
21109
 
21110
+ @if (form.chartType === 'area') {
21111
+ <div class="form-field checkbox-field">
21112
+ <label>
21113
+ <input type="checkbox" [(ngModel)]="form.stacked" kendoCheckBox />
21114
+ Stacked
21115
+ </label>
21116
+ </div>
21117
+ <p class="field-hint">Series stack in their order; a bucket without data stays a gap.</p>
21118
+ }
21119
+
19013
21120
  <div class="form-row">
19014
21121
  <div class="form-field checkbox-field">
19015
21122
  <label>
@@ -19113,6 +21220,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
19113
21220
  type: Input
19114
21221
  }], initialFilters: [{
19115
21222
  type: Input
21223
+ }], initialAdditionalSeries: [{
21224
+ type: Input
21225
+ }], initialStacked: [{
21226
+ type: Input
21227
+ }], initialPrimarySeries: [{
21228
+ type: Input
19116
21229
  }] } });
19117
21230
 
19118
21231
  /**
@@ -19479,6 +21592,22 @@ class HeatmapWidgetComponent {
19479
21592
  numericValue = 0;
19480
21593
  }
19481
21594
  }
21595
+ // A null value cell is "no data" (e.g. an Integral over an empty window, AB#7975): the
21596
+ // row is skipped instead of counting as 0 in its bucket (sum/avg). The value cell is
21597
+ // resolved exactly (`findCellForField`), so a null `<field>_integral` never hides or
21598
+ // overwrites a usable `<field>`.
21599
+ // A count ignores the value; a weighted mean validates value and weight below.
21600
+ if (valueField && aggregation !== 'count' && !weighted) {
21601
+ const valueCell = findCellForField(row.cells, valueField);
21602
+ if (valueCell) {
21603
+ const val = valueCell.value;
21604
+ if (val === null || val === undefined)
21605
+ continue;
21606
+ numericValue = typeof val === 'number' ? val : parseFloat(String(val));
21607
+ if (isNaN(numericValue))
21608
+ numericValue = 0;
21609
+ }
21610
+ }
19482
21611
  // Stream-data rows carry their time at top level (`row.timestamp`, see
19483
21612
  // QueryExecutorService.mapStreamDataRows); the cells only hold the value
19484
21613
  // columns. Same source the line chart uses for the bin time (AB#7938).
@@ -20111,8 +22240,10 @@ class HeatmapConfigDialogComponent {
20111
22240
  aggregationType: c.aggregationType ?? null
20112
22241
  }));
20113
22242
  }
20114
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
20115
- 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 => ({
20116
22247
  attributePath: c.attributePath,
20117
22248
  attributeValueType: c.attributeValueType ?? '',
20118
22249
  aggregationType: c.aggregationType ?? null
@@ -23139,8 +25270,10 @@ class WidgetGroupConfigDialogComponent {
23139
25270
  aggregationType: c.aggregationType ?? null
23140
25271
  }));
23141
25272
  }
23142
- const sdResult = await firstValueFrom(this.queryExecutor.executeStreamData(rtId, { first: 1 }));
23143
- 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 => ({
23144
25277
  attributePath: c.attributePath,
23145
25278
  attributeValueType: c.attributeValueType ?? '',
23146
25279
  aggregationType: c.aggregationType ?? null
@@ -25982,6 +28115,332 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
25982
28115
  `, changeDetection: ChangeDetectionStrategy.Eager, styles: [".config-form{display:flex;flex-direction:column;gap:12px;padding:16px}.form-group{display:flex;flex-direction:column;gap:4px}.form-row{display:flex;gap:16px}.form-row .form-group{flex:1}.hint{font-size:.75rem;opacity:.6}\n"] }]
25983
28116
  }] });
25984
28117
 
28118
+ /**
28119
+ * Process Widget Type identifier
28120
+ */
28121
+ const PROCESS_WIDGET_TYPE = 'process';
28122
+ /**
28123
+ * Name of the implicit binding a config without `bindings` is read as: the
28124
+ * legacy single-binding fields (`dataBindingMode`, `bindingCkTypeId`, …).
28125
+ * Mappings without `bindingName` refer to it.
28126
+ */
28127
+ const DEFAULT_PROCESS_BINDING_NAME = 'default';
28128
+ /**
28129
+ * Default configuration for a new Process Widget
28130
+ */
28131
+ const DEFAULT_PROCESS_WIDGET_CONFIG = {
28132
+ type: 'process',
28133
+ fitToBounds: true,
28134
+ allowZoom: false,
28135
+ allowPan: false,
28136
+ showToolbar: false,
28137
+ initialZoom: 1
28138
+ };
28139
+
28140
+ /**
28141
+ * Aggregation of a mapped Process Widget value over all rows of a query
28142
+ * binding's selector scope (AB#8024): `first` (default, the first row as
28143
+ * before), `sum`, `avg`, `min`, `max`.
28144
+ *
28145
+ * Aggregation works on **query bindings only**: a runtime-entity binding loads
28146
+ * a single entity, so there is nothing to aggregate. To aggregate over all
28147
+ * entities of a scope, bind a runtime persistent query with the entity
28148
+ * selector instead.
28149
+ */
28150
+ /** All aggregates a mapping may name. */
28151
+ const PROCESS_VALUE_AGGREGATES = ['first', 'sum', 'avg', 'min', 'max'];
28152
+ /** `true` when the aggregate reduces several rows (anything but `first`/absent). */
28153
+ function isReducingAggregate(aggregate) {
28154
+ return aggregate !== undefined && aggregate !== 'first';
28155
+ }
28156
+ /**
28157
+ * Reads one cell of a query row: the column name, or its sanitized form
28158
+ * (dots → underscores) as the query service delivers nested paths.
28159
+ */
28160
+ function cellValue(cells, columnName) {
28161
+ return cells.get(columnName) ?? cells.get(columnName.replace(/\./g, '_'));
28162
+ }
28163
+ /** A finite number from a cell, or `undefined` for null/undefined/non-numeric cells. */
28164
+ function numericCell(value) {
28165
+ if (typeof value === 'number') {
28166
+ return Number.isFinite(value) ? value : undefined;
28167
+ }
28168
+ if (typeof value === 'string' && value.trim() !== '') {
28169
+ const parsed = Number(value);
28170
+ return Number.isFinite(parsed) ? parsed : undefined;
28171
+ }
28172
+ return undefined;
28173
+ }
28174
+ /**
28175
+ * The value of a column over the query rows.
28176
+ *
28177
+ * - `first` / absent: the cell of the first row, unchanged (today's behaviour).
28178
+ * - `sum`, `avg`, `min`, `max`: over the numeric cells; null, undefined and
28179
+ * non-numeric cells are skipped. Zero rows or no numeric cell ⇒ `undefined`
28180
+ * (the property keeps no value, not 0).
28181
+ */
28182
+ function aggregateQueryColumn(queryResult, columnName, aggregate) {
28183
+ const rows = queryResult.rows ?? [];
28184
+ if (rows.length === 0) {
28185
+ return undefined;
28186
+ }
28187
+ if (!isReducingAggregate(aggregate)) {
28188
+ return cellValue(rows[0].cells, columnName);
28189
+ }
28190
+ const numbers = [];
28191
+ for (const row of rows) {
28192
+ const n = numericCell(cellValue(row.cells, columnName));
28193
+ if (n !== undefined) {
28194
+ numbers.push(n);
28195
+ }
28196
+ }
28197
+ if (numbers.length === 0) {
28198
+ return undefined;
28199
+ }
28200
+ switch (aggregate) {
28201
+ case 'sum':
28202
+ return numbers.reduce((a, b) => a + b, 0);
28203
+ case 'avg':
28204
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
28205
+ case 'min':
28206
+ return Math.min(...numbers);
28207
+ case 'max':
28208
+ return Math.max(...numbers);
28209
+ default:
28210
+ return undefined;
28211
+ }
28212
+ }
28213
+ /**
28214
+ * `true` when a reducing aggregate ran over fewer rows than the scope holds
28215
+ * (`totalCount` > loaded rows) — the result is partial and worth a warning.
28216
+ */
28217
+ function isPartialAggregate(queryResult) {
28218
+ const loaded = queryResult.rows?.length ?? 0;
28219
+ return typeof queryResult.totalCount === 'number' && queryResult.totalCount > loaded;
28220
+ }
28221
+ /**
28222
+ * Why a mapping's aggregate cannot apply, or `undefined` when it can (or is
28223
+ * `first`/absent). A reducing aggregate needs a `number` property and a
28224
+ * persistent-query binding. The widget falls back to `first` in those cases.
28225
+ *
28226
+ * @param propertyType Type of the diagram's transform property, when known.
28227
+ * @param bindingMode Mode of the mapping's binding, when known.
28228
+ */
28229
+ function processAggregateIssue(aggregate, propertyType, bindingMode) {
28230
+ if (aggregate === undefined) {
28231
+ return undefined;
28232
+ }
28233
+ if (!PROCESS_VALUE_AGGREGATES.includes(aggregate)) {
28234
+ return `Unknown aggregate '${String(aggregate)}'`;
28235
+ }
28236
+ if (!isReducingAggregate(aggregate)) {
28237
+ return undefined;
28238
+ }
28239
+ if (bindingMode === 'runtimeEntity') {
28240
+ return `Aggregate '${aggregate}' needs a persistent query binding; a runtime entity binding loads one entity`;
28241
+ }
28242
+ if (propertyType !== undefined && propertyType !== 'number') {
28243
+ return `Aggregate '${aggregate}' needs a number property, not '${propertyType}'`;
28244
+ }
28245
+ return undefined;
28246
+ }
28247
+
28248
+ /**
28249
+ * The legacy single-binding fields read as one binding named
28250
+ * {@link DEFAULT_PROCESS_BINDING_NAME}, or `undefined` when the legacy mode is
28251
+ * absent or `'none'`.
28252
+ */
28253
+ function legacyProcessBinding(config) {
28254
+ const mode = config.dataBindingMode;
28255
+ if (!mode || mode === 'none') {
28256
+ return undefined;
28257
+ }
28258
+ return {
28259
+ name: DEFAULT_PROCESS_BINDING_NAME,
28260
+ mode,
28261
+ ckTypeId: config.bindingCkTypeId,
28262
+ rtId: config.bindingRtId,
28263
+ queryRtId: config.bindingQueryRtId,
28264
+ queryName: config.bindingQueryName,
28265
+ queryFamily: config.bindingQueryFamily,
28266
+ ignoreTimeFilter: config.bindingIgnoreTimeFilter,
28267
+ entitySelectorId: config.bindingEntitySelectorId,
28268
+ filters: config.bindingFilters
28269
+ };
28270
+ }
28271
+ /** `true` when the config uses the named `bindings` list instead of the legacy fields. */
28272
+ function hasNamedProcessBindings(config) {
28273
+ return Array.isArray(config.bindings) && config.bindings.length > 0;
28274
+ }
28275
+ /**
28276
+ * The bindings the widget loads: the named `bindings` when present, otherwise
28277
+ * the implicit legacy binding (or none).
28278
+ */
28279
+ function resolveProcessBindings(config) {
28280
+ if (hasNamedProcessBindings(config)) {
28281
+ return config.bindings;
28282
+ }
28283
+ const legacy = legacyProcessBinding(config);
28284
+ return legacy ? [legacy] : [];
28285
+ }
28286
+ /** The binding name a mapping reads from (`default` when absent). */
28287
+ function mappingBindingName(mapping) {
28288
+ return mapping.bindingName || DEFAULT_PROCESS_BINDING_NAME;
28289
+ }
28290
+ /**
28291
+ * The config problem of a single binding, or `undefined` when it can load.
28292
+ * A runtime-entity binding needs a CK type (or a selector to take it from) and
28293
+ * an rtId or an entity selector; a query binding needs a query rtId.
28294
+ */
28295
+ function processBindingConfigError(binding) {
28296
+ if (binding.mode === 'runtimeEntity') {
28297
+ if (!binding.rtId && !binding.entitySelectorId) {
28298
+ return 'Runtime entity binding needs an rtId or an entity selector';
28299
+ }
28300
+ if (!binding.ckTypeId && !binding.entitySelectorId) {
28301
+ return 'Runtime entity binding needs a CK type';
28302
+ }
28303
+ return undefined;
28304
+ }
28305
+ if (binding.mode === 'persistentQuery') {
28306
+ return binding.queryRtId ? undefined : 'Persistent query binding needs a query rtId';
28307
+ }
28308
+ return `Unknown binding mode '${String(binding.mode)}'`;
28309
+ }
28310
+ /**
28311
+ * Validates the bindings and mappings of a Process Widget config (AB#8023):
28312
+ * empty or duplicate binding names, bindings that cannot load (see
28313
+ * {@link processBindingConfigError}), selectors that do not exist on the board
28314
+ * (only checked when `knownSelectorIds` is given), and mappings that refer to an
28315
+ * unknown binding. A legacy config is validated like its implicit binding; a
28316
+ * config without any binding has no issues.
28317
+ *
28318
+ * Aggregates (AB#8024): an unknown aggregate, or one other than `first` on a
28319
+ * runtime-entity binding, an `attribute` mapping or — when `propertyTypes` is
28320
+ * given — a non-`number` property, is reported; the widget then reads the
28321
+ * first row.
28322
+ *
28323
+ * @param propertyTypes Type per diagram property id (`transformProperties`), when the diagram is known.
28324
+ */
28325
+ function validateProcessWidgetBindings(config, knownSelectorIds, propertyTypes) {
28326
+ const issues = [];
28327
+ const bindings = resolveProcessBindings(config);
28328
+ const names = new Set();
28329
+ for (const binding of bindings) {
28330
+ if (!binding.name || !binding.name.trim()) {
28331
+ issues.push({ message: 'A binding has no name' });
28332
+ continue;
28333
+ }
28334
+ if (names.has(binding.name)) {
28335
+ issues.push({ bindingName: binding.name, message: `Duplicate binding name '${binding.name}'` });
28336
+ }
28337
+ names.add(binding.name);
28338
+ const error = processBindingConfigError(binding);
28339
+ if (error) {
28340
+ issues.push({ bindingName: binding.name, message: error });
28341
+ }
28342
+ if (binding.entitySelectorId && knownSelectorIds && !knownSelectorIds.includes(binding.entitySelectorId)) {
28343
+ issues.push({
28344
+ bindingName: binding.name,
28345
+ message: `Entity selector '${binding.entitySelectorId}' does not exist on the board`
28346
+ });
28347
+ }
28348
+ }
28349
+ // Only report unknown names when the widget has bindings at all — a widget
28350
+ // without data binding keeps stale mappings silently, as before.
28351
+ if (bindings.length > 0) {
28352
+ for (const mapping of config.propertyMappings ?? []) {
28353
+ const name = mappingBindingName(mapping);
28354
+ if (!names.has(name)) {
28355
+ issues.push({
28356
+ bindingName: name,
28357
+ propertyId: mapping.propertyId,
28358
+ message: `Mapping of '${mapping.propertyId}' refers to unknown binding '${name}'`
28359
+ });
28360
+ }
28361
+ const aggregateIssue = mappingAggregateIssue(mapping, bindings.find(b => b.name === name), propertyTypes);
28362
+ if (aggregateIssue) {
28363
+ issues.push({
28364
+ bindingName: name,
28365
+ propertyId: mapping.propertyId,
28366
+ message: `Mapping of '${mapping.propertyId}': ${aggregateIssue}; the first row is used`
28367
+ });
28368
+ }
28369
+ }
28370
+ }
28371
+ return issues;
28372
+ }
28373
+ /**
28374
+ * Why a mapping's `aggregate` cannot apply (AB#8024), or `undefined`. Besides
28375
+ * {@link processAggregateIssue} an `attribute` mapping cannot aggregate: it
28376
+ * reads one entity.
28377
+ */
28378
+ function mappingAggregateIssue(mapping, binding, propertyTypes) {
28379
+ const issue = processAggregateIssue(mapping.aggregate, propertyTypes?.get(mapping.propertyId), binding?.mode);
28380
+ if (issue) {
28381
+ return issue;
28382
+ }
28383
+ if (mapping.aggregate && mapping.aggregate !== 'first' && mapping.sourceType === 'attribute') {
28384
+ return `Aggregate '${mapping.aggregate}' needs a column mapping; an attribute mapping reads one entity`;
28385
+ }
28386
+ return undefined;
28387
+ }
28388
+ /**
28389
+ * Reads persisted `bindings` (JSON/YAML seed) into typed bindings. Entries that
28390
+ * are not objects or have an unknown mode are dropped; returns `undefined` when
28391
+ * nothing usable is left, so a config without bindings stays without the key.
28392
+ */
28393
+ function parsePersistedProcessBindings(raw) {
28394
+ if (!Array.isArray(raw)) {
28395
+ return undefined;
28396
+ }
28397
+ const bindings = [];
28398
+ for (const entry of raw) {
28399
+ if (!entry || typeof entry !== 'object') {
28400
+ continue;
28401
+ }
28402
+ const e = entry;
28403
+ if (e['mode'] !== 'runtimeEntity' && e['mode'] !== 'persistentQuery') {
28404
+ console.warn('[ProcessWidget] Ignoring binding with unknown mode:', e['name'], e['mode']);
28405
+ continue;
28406
+ }
28407
+ bindings.push(compactBinding({
28408
+ name: typeof e['name'] === 'string' ? e['name'] : '',
28409
+ mode: e['mode'],
28410
+ ckTypeId: e['ckTypeId'],
28411
+ rtId: e['rtId'],
28412
+ queryRtId: e['queryRtId'],
28413
+ queryName: e['queryName'],
28414
+ queryFamily: e['queryFamily'],
28415
+ ignoreTimeFilter: e['ignoreTimeFilter'],
28416
+ entitySelectorId: e['entitySelectorId'],
28417
+ filters: e['filters']
28418
+ }));
28419
+ }
28420
+ return bindings.length > 0 ? bindings : undefined;
28421
+ }
28422
+ /**
28423
+ * Serializes bindings for persistence: only set keys are written
28424
+ * (`ignoreTimeFilter` only when `true`, `filters` only when non-empty).
28425
+ */
28426
+ function toPersistedProcessBindings(bindings) {
28427
+ return bindings.map(compactBinding);
28428
+ }
28429
+ function compactBinding(binding) {
28430
+ return {
28431
+ name: binding.name,
28432
+ mode: binding.mode,
28433
+ ...(binding.ckTypeId && { ckTypeId: binding.ckTypeId }),
28434
+ ...(binding.rtId && { rtId: binding.rtId }),
28435
+ ...(binding.queryRtId && { queryRtId: binding.queryRtId }),
28436
+ ...(binding.queryName && { queryName: binding.queryName }),
28437
+ ...(binding.queryFamily && { queryFamily: binding.queryFamily }),
28438
+ ...(binding.ignoreTimeFilter === true && { ignoreTimeFilter: true }),
28439
+ ...(binding.entitySelectorId && { entitySelectorId: binding.entitySelectorId }),
28440
+ ...(binding.filters && binding.filters.length > 0 && { filters: binding.filters })
28441
+ };
28442
+ }
28443
+
25985
28444
  class ProcessDataService {
25986
28445
  dataService = inject(MeshBoardDataService);
25987
28446
  variableService = inject(MeshBoardVariableService);
@@ -26447,51 +28906,90 @@ class ProcessDataService {
26447
28906
  // Widget Data Binding Methods
26448
28907
  // ============================================================================
26449
28908
  /**
26450
- * Loads bound data based on widget configuration.
26451
- * 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}).
26452
28911
  *
26453
28912
  * @param config - The process widget configuration with data binding settings
26454
28913
  * @returns BoundDataResult containing the loaded data, or null if no binding configured
26455
28914
  */
26456
28915
  async loadBoundData(config) {
26457
- if (!config.dataBindingMode || config.dataBindingMode === 'none') {
26458
- return null;
26459
- }
26460
- try {
26461
- if (config.dataBindingMode === 'runtimeEntity') {
26462
- 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]);
26463
28933
  }
26464
- if (config.dataBindingMode === 'persistentQuery') {
26465
- 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);
26466
28946
  }
26467
- return null;
28947
+ const result = binding.mode === 'runtimeEntity'
28948
+ ? await this.loadBoundRuntimeEntityData(binding)
28949
+ : await this.loadBoundQueryData(binding);
28950
+ return { ...result, bindingName: binding.name };
26468
28951
  }
26469
28952
  catch (error) {
26470
- console.error('[ProcessDataService] Error loading bound data:', error);
28953
+ console.error(`[ProcessDataService] Error loading binding '${binding.name}':`, error);
26471
28954
  return {
26472
- type: config.dataBindingMode === 'runtimeEntity' ? 'entity' : 'query',
28955
+ type,
28956
+ bindingName: binding.name,
26473
28957
  error: error instanceof Error ? error.message : 'Unknown error',
26474
28958
  loadedAt: new Date()
26475
28959
  };
26476
28960
  }
26477
28961
  }
26478
28962
  /**
26479
- * Loads data from a runtime entity binding
28963
+ * Loads data from a runtime entity binding. A fixed `rtId` (variables
28964
+ * resolved) wins; without one the current entity of the bound selector is
28965
+ * loaded (AB#8023): the first rtId of its resolved scope. A pending scope
28966
+ * gives a `pending` result, an empty scope or a selector without pick no
28967
+ * entity (no value, never the previous one).
26480
28968
  */
26481
- async loadBoundRuntimeEntityData(config) {
26482
- const { bindingCkTypeId, bindingRtId } = config;
26483
- if (!bindingCkTypeId) {
26484
- 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);
26485
28974
  }
26486
- if (!bindingRtId) {
26487
- 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');
26488
28988
  }
26489
- // Resolve variables in rtId (e.g., ${selectedEntity})
26490
- const resolvedRtId = this.resolveVariables(bindingRtId);
26491
28989
  // Fetch the entity
26492
- const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(resolvedRtId, bindingCkTypeId));
28990
+ const entity = await firstValueFrom(this.dataService.fetchEntityWithAssociations(rtId, ckTypeId));
26493
28991
  if (!entity) {
26494
- throw new Error(`Entity not found: ${resolvedRtId}`);
28992
+ throw new Error(`Entity not found: ${rtId}`);
26495
28993
  }
26496
28994
  return {
26497
28995
  type: 'entity',
@@ -26499,39 +28997,50 @@ class ProcessDataService {
26499
28997
  loadedAt: new Date()
26500
28998
  };
26501
28999
  }
29000
+ /**
29001
+ * CK type of the entity a selector's scope consists of — known only for a
29002
+ * selector without `childScope` (its scope is the picked entity itself).
29003
+ */
29004
+ selectorCkTypeId(selectorId) {
29005
+ if (!selectorId) {
29006
+ return undefined;
29007
+ }
29008
+ const selector = this.stateService.getConfig().entitySelectors?.find(s => s.id === selectorId);
29009
+ return selector && !selector.childScope ? selector.ckTypeId : undefined;
29010
+ }
26502
29011
  /**
26503
29012
  * Loads data from a persistent query binding
26504
29013
  */
26505
- async loadBoundQueryData(config) {
26506
- const { bindingQueryRtId, bindingFilters, bindingQueryFamily } = config;
26507
- if (!bindingQueryRtId) {
29014
+ async loadBoundQueryData(binding) {
29015
+ const { queryRtId, filters, queryFamily } = binding;
29016
+ if (!queryRtId) {
26508
29017
  throw new Error('Persistent query binding requires a Query rtId');
26509
29018
  }
26510
29019
  // Entity-selector scope (AB#7721): NO query is sent while the scope is pending
26511
29020
  // or empty — never the persisted (unscoped) scope. Pending → a `pending` result
26512
29021
  // (the widget stays loading; a refresh follows once the scope resolves);
26513
29022
  // empty → no rows.
26514
- const scope = this.stateService.resolveStreamDataScope(config.bindingEntitySelectorId, bindingQueryFamily, config.bindingIgnoreTimeFilter);
29023
+ const scope = this.stateService.resolveStreamDataScope(binding.entitySelectorId, queryFamily, binding.ignoreTimeFilter);
26515
29024
  if (scope.kind === 'pending') {
26516
29025
  return { type: 'query', pending: true, loadedAt: new Date() };
26517
29026
  }
26518
29027
  if (scope.kind === 'empty') {
26519
29028
  return {
26520
29029
  type: 'query',
26521
- queryResult: this.mapExecutionResult(emptyQueryExecutionResult(bindingQueryFamily)),
29030
+ queryResult: this.mapExecutionResult(emptyQueryExecutionResult(queryFamily)),
26522
29031
  loadedAt: new Date()
26523
29032
  };
26524
29033
  }
26525
29034
  // Convert widget filters to GraphQL format with variable resolution
26526
- const fieldFilters = this.convertFiltersToDto(bindingFilters);
29035
+ const fieldFilters = this.convertFiltersToDto(filters);
26527
29036
  // Execute via the unified executor — it routes runtime vs stream-data from
26528
29037
  // the persisted family (or a one-time lookup when absent). Stream-data
26529
29038
  // queries additionally consume the time-filter / asset-scope overrides built
26530
29039
  // below; runtime queries ignore `streamDataArgs`.
26531
- const result = await firstValueFrom(this.queryExecutor.execute(bindingQueryFamily, bindingQueryRtId, {
29040
+ const result = await firstValueFrom(this.queryExecutor.execute(queryFamily, queryRtId, {
26532
29041
  first: 1000, // Reasonable limit for process diagram data
26533
29042
  fieldFilter: fieldFilters,
26534
- streamDataArgs: this.buildBindingStreamDataArgs(config),
29043
+ streamDataArgs: this.buildBindingStreamDataArgs(binding),
26535
29044
  // Runtime queries: `rtId IN` field filter (AB#7725); stream data: arg.rtIds.
26536
29045
  scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
26537
29046
  }));
@@ -26547,9 +29056,9 @@ class ProcessDataService {
26547
29056
  * Returns `undefined` when neither override applies (e.g. runtime query, no
26548
29057
  * time filter, opted out). Runtime queries ignore the result.
26549
29058
  */
26550
- buildBindingStreamDataArgs(config) {
26551
- const timeArgs = this.stateService.resolveStreamDataTimeArgs(config.bindingIgnoreTimeFilter);
26552
- 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);
26553
29062
  if (!timeArgs && !rtIds) {
26554
29063
  return undefined;
26555
29064
  }
@@ -26627,9 +29136,12 @@ class ProcessWidgetComponent {
26627
29136
  config;
26628
29137
  containerRef;
26629
29138
  dataService = inject(ProcessDataService);
29139
+ stateService = inject(MeshBoardStateService);
26630
29140
  symbolLibraryService = inject(SymbolLibraryService);
26631
29141
  sanitizer = inject(DomSanitizer);
26632
29142
  expressionEvaluator = inject(ExpressionEvaluatorService);
29143
+ /** Host language for `text.value` bindings (AB#8022); no language = de-AT. */
29144
+ hostLanguage = injectMeshBoardLanguage();
26633
29145
  // Math reference for template
26634
29146
  Math = Math;
26635
29147
  // State signals
@@ -26645,6 +29157,16 @@ class ProcessWidgetComponent {
26645
29157
  ...(ngDevMode ? [{ debugName: "_symbolDefinitions" }] : /* istanbul ignore next */ []));
26646
29158
  _boundData = signal(null, /* @ts-ignore */
26647
29159
  ...(ngDevMode ? [{ debugName: "_boundData" }] : /* istanbul ignore next */ []));
29160
+ /** Loaded results per binding name (AB#8023); pending bindings are absent. */
29161
+ _boundDataByBinding = signal(new Map(), /* @ts-ignore */
29162
+ ...(ngDevMode ? [{ debugName: "_boundDataByBinding" }] : /* istanbul ignore next */ []));
29163
+ /** Console messages already written by this widget instance (each is reported once). */
29164
+ reportedWarnings = new Set();
29165
+ /**
29166
+ * Generation of the latest `loadDiagram()` call: a slower, older load (e.g. of
29167
+ * the previous leader pick) never overwrites the bound data of a newer one.
29168
+ */
29169
+ loadGeneration = 0;
26648
29170
  isLoading = this._isLoading.asReadonly();
26649
29171
  error = this._error.asReadonly();
26650
29172
  diagramConfig = this._diagramConfig.asReadonly();
@@ -26655,6 +29177,30 @@ class ProcessWidgetComponent {
26655
29177
  * Contains entity or query data that can be used for dynamic values in the diagram.
26656
29178
  */
26657
29179
  boundData = this._boundData.asReadonly();
29180
+ /** Loaded results per binding name (AB#8023). Pending bindings have no entry. */
29181
+ boundDataByBinding = this._boundDataByBinding.asReadonly();
29182
+ /**
29183
+ * Bindings whose load failed, for the error badge (AB#8023). Only for widgets
29184
+ * with named `bindings`: a legacy single-binding widget keeps its previous
29185
+ * behaviour (diagram without values, no badge).
29186
+ */
29187
+ bindingErrors = computed(() => {
29188
+ const errors = [];
29189
+ const results = this._boundDataByBinding();
29190
+ if (!hasNamedProcessBindings(this.config)) {
29191
+ return errors;
29192
+ }
29193
+ for (const [name, result] of results) {
29194
+ if (result.error) {
29195
+ errors.push({ name, message: result.error });
29196
+ }
29197
+ }
29198
+ return errors;
29199
+ }, /* @ts-ignore */
29200
+ ...(ngDevMode ? [{ debugName: "bindingErrors" }] : /* istanbul ignore next */ []));
29201
+ /** Text of the error badge: "Binding pv: <message>; Binding netz: …". */
29202
+ bindingErrorText = computed(() => this.bindingErrors().map(e => `Binding ${e.name}: ${e.message}`).join('; '), /* @ts-ignore */
29203
+ ...(ngDevMode ? [{ debugName: "bindingErrorText" }] : /* istanbul ignore next */ []));
26658
29204
  // Computed SVG properties
26659
29205
  viewBox = computed(() => {
26660
29206
  const diagram = this._diagramConfig();
@@ -26682,7 +29228,7 @@ class ProcessWidgetComponent {
26682
29228
  animationEnabledStates = computed(() => {
26683
29229
  const states = {};
26684
29230
  const diagram = this._diagramConfig();
26685
- const boundData = this._boundData();
29231
+ const boundData = this._boundDataByBinding();
26686
29232
  if (!diagram?.propertyBindings || diagram.propertyBindings.length === 0) {
26687
29233
  return states;
26688
29234
  }
@@ -26692,7 +29238,7 @@ class ProcessWidgetComponent {
26692
29238
  return states;
26693
29239
  }
26694
29240
  // Build property values from bound data
26695
- const propertyValues = boundData
29241
+ const propertyValues = boundData.size > 0
26696
29242
  ? this.buildPropertyValuesFromBoundData(diagram.transformProperties ?? [], this.config.propertyMappings ?? [], boundData)
26697
29243
  : {};
26698
29244
  // Evaluate each animation binding
@@ -26720,18 +29266,24 @@ class ProcessWidgetComponent {
26720
29266
  */
26721
29267
  boundPrimitives = computed(() => {
26722
29268
  const diagram = this._diagramConfig();
26723
- const boundData = this._boundData();
29269
+ const boundData = this._boundDataByBinding();
26724
29270
  if (!diagram?.primitives) {
26725
29271
  return [];
26726
29272
  }
26727
- // If no bindings or no bound data, return original primitives
26728
- 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')) {
26729
29280
  return diagram.primitives;
26730
29281
  }
26731
29282
  // Build property values from bound data using mappings
26732
- 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);
26733
29285
  // Apply bindings to primitives
26734
- return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues);
29286
+ return this.applyBindingsToPrimitives(diagram.primitives, diagram.propertyBindings, propertyValues, diagram.transformProperties, noValue);
26735
29287
  }, /* @ts-ignore */
26736
29288
  ...(ngDevMode ? [{ debugName: "boundPrimitives" }] : /* istanbul ignore next */ []));
26737
29289
  ngOnInit() {
@@ -26749,9 +29301,11 @@ class ProcessWidgetComponent {
26749
29301
  * Loads the process diagram configuration and data
26750
29302
  */
26751
29303
  async loadDiagram() {
29304
+ const generation = ++this.loadGeneration;
26752
29305
  this._isLoading.set(true);
26753
29306
  this._error.set(null);
26754
29307
  this._boundData.set(null);
29308
+ this._boundDataByBinding.set(new Map());
26755
29309
  // A bound entity-selector scope that is still resolving keeps the widget loading
26756
29310
  // until the refresh that follows the resolution (AB#7721).
26757
29311
  let scopePending = false;
@@ -26761,7 +29315,7 @@ class ProcessWidgetComponent {
26761
29315
  this._diagramConfig.set(this.config.inlineConfig);
26762
29316
  await this.loadSymbolDefinitions(this.config.inlineConfig);
26763
29317
  await this.loadRuntimeData();
26764
- scopePending = await this.loadBoundData();
29318
+ scopePending = await this.loadBoundData(generation);
26765
29319
  return;
26766
29320
  }
26767
29321
  // Load from backend by rtId
@@ -26770,7 +29324,7 @@ class ProcessWidgetComponent {
26770
29324
  this._diagramConfig.set(diagram);
26771
29325
  await this.loadSymbolDefinitions(diagram);
26772
29326
  await this.loadRuntimeData();
26773
- scopePending = await this.loadBoundData();
29327
+ scopePending = await this.loadBoundData(generation);
26774
29328
  return;
26775
29329
  }
26776
29330
  // No configuration
@@ -26781,7 +29335,7 @@ class ProcessWidgetComponent {
26781
29335
  this._error.set('Failed to load process diagram');
26782
29336
  }
26783
29337
  finally {
26784
- if (!scopePending) {
29338
+ if (!scopePending && generation === this.loadGeneration) {
26785
29339
  this._isLoading.set(false);
26786
29340
  }
26787
29341
  }
@@ -26790,17 +29344,34 @@ class ProcessWidgetComponent {
26790
29344
  * Loads bound data based on widget data binding configuration.
26791
29345
  * Returns `true` when the bound entity-selector scope is still resolving (AB#7721).
26792
29346
  */
26793
- async loadBoundData() {
26794
- // Only load if data binding is configured
26795
- if (!this.config.dataBindingMode || this.config.dataBindingMode === 'none') {
26796
- return false;
26797
- }
29347
+ async loadBoundData(generation) {
29348
+ this.reportBindingIssues();
26798
29349
  try {
26799
- const boundData = await this.dataService.loadBoundData(this.config);
26800
- 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) {
26801
29371
  return true;
26802
29372
  }
26803
- this._boundData.set(boundData);
29373
+ this._boundDataByBinding.set(loaded);
29374
+ this._boundData.set(loaded.get(DEFAULT_PROCESS_BINDING_NAME) ?? null);
26804
29375
  }
26805
29376
  catch (err) {
26806
29377
  console.error('Error loading bound data:', err);
@@ -26808,6 +29379,22 @@ class ProcessWidgetComponent {
26808
29379
  }
26809
29380
  return false;
26810
29381
  }
29382
+ /** Writes the config issues of the bindings to the console, each once per widget instance. */
29383
+ reportBindingIssues() {
29384
+ const selectorIds = (this.stateService.getConfig().entitySelectors ?? []).map(s => s.id);
29385
+ const properties = this._diagramConfig()?.transformProperties;
29386
+ const propertyTypes = properties ? new Map(properties.map(p => [p.id, p.type])) : undefined;
29387
+ for (const issue of validateProcessWidgetBindings(this.config, selectorIds, propertyTypes)) {
29388
+ this.warnOnce(`[ProcessWidget] ${this.config.title || this.config.id}: ${issue.message}`);
29389
+ }
29390
+ }
29391
+ warnOnce(message) {
29392
+ if (this.reportedWarnings.has(message)) {
29393
+ return;
29394
+ }
29395
+ this.reportedWarnings.add(message);
29396
+ console.warn(message);
29397
+ }
26811
29398
  /**
26812
29399
  * Loads symbol definitions for all symbol instances in the diagram
26813
29400
  */
@@ -27343,11 +29930,12 @@ class ProcessWidgetComponent {
27343
29930
  return [];
27344
29931
  }
27345
29932
  // Get property values for this symbol instance
27346
- const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition);
29933
+ const noValue = new Set();
29934
+ const propertyValues = this.getSymbolInstancePropertyValues(symbolInstance, definition, noValue);
27347
29935
  // If there are bindings, apply them
27348
29936
  let primitives;
27349
29937
  if (definition.propertyBindings && definition.propertyBindings.length > 0) {
27350
- primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues);
29938
+ primitives = this.applyBindingsToPrimitives(definition.primitives, definition.propertyBindings, propertyValues, definition.transformProperties, noValue);
27351
29939
  }
27352
29940
  else {
27353
29941
  primitives = [...definition.primitives];
@@ -27355,7 +29943,7 @@ class ProcessWidgetComponent {
27355
29943
  // Add primitives from nested symbol instances (recursively)
27356
29944
  const nestedInstances = definition.symbolInstances ?? [];
27357
29945
  for (const nestedInst of nestedInstances) {
27358
- const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues);
29946
+ const nestedPrimitives = this.getNestedSymbolPrimitives(nestedInst, definition, propertyValues, noValue);
27359
29947
  primitives.push(...nestedPrimitives);
27360
29948
  }
27361
29949
  return primitives;
@@ -27363,18 +29951,19 @@ class ProcessWidgetComponent {
27363
29951
  /**
27364
29952
  * Gets primitives for a nested symbol instance, with position offset applied.
27365
29953
  */
27366
- getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues) {
29954
+ getNestedSymbolPrimitives(nestedInst, parentDef, parentPropertyValues, parentNoValue = new Set()) {
27367
29955
  const definitions = this._symbolDefinitions();
27368
29956
  const nestedDef = definitions.get(nestedInst.symbolRtId);
27369
29957
  if (!nestedDef?.primitives) {
27370
29958
  return [];
27371
29959
  }
27372
29960
  // Calculate property values for the nested instance using "Pass to child property" bindings
27373
- const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues);
29961
+ const nestedNoValue = new Set();
29962
+ const nestedPropertyValues = this.calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue, nestedNoValue);
27374
29963
  // Apply bindings to nested primitives
27375
29964
  let nestedPrimitives;
27376
29965
  if (nestedDef.propertyBindings && nestedDef.propertyBindings.length > 0) {
27377
- nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues);
29966
+ nestedPrimitives = this.applyBindingsToPrimitives(nestedDef.primitives, nestedDef.propertyBindings, nestedPropertyValues, nestedDef.transformProperties, nestedNoValue);
27378
29967
  }
27379
29968
  else {
27380
29969
  nestedPrimitives = [...nestedDef.primitives];
@@ -27392,7 +29981,7 @@ class ProcessWidgetComponent {
27392
29981
  // Recursively handle nested instances within the nested symbol
27393
29982
  const deepNestedInstances = nestedDef.symbolInstances ?? [];
27394
29983
  for (const deepNestedInst of deepNestedInstances) {
27395
- const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues);
29984
+ const deepPrimitives = this.getNestedSymbolPrimitives(deepNestedInst, nestedDef, nestedPropertyValues, nestedNoValue);
27396
29985
  // Apply the parent offset to deep nested primitives
27397
29986
  const deepOffsetPrimitives = deepPrimitives.map(prim => ({
27398
29987
  ...prim,
@@ -27408,7 +29997,7 @@ class ProcessWidgetComponent {
27408
29997
  /**
27409
29998
  * Calculate property values for a nested symbol instance based on "Pass to child property" bindings.
27410
29999
  */
27411
- calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues) {
30000
+ calculateNestedPropertyValues(parentDef, nestedInst, parentPropertyValues, parentNoValue = new Set(), nestedNoValue) {
27412
30001
  const nestedPropertyValues = {};
27413
30002
  // Get default values from nested symbol definition
27414
30003
  const definitions = this._symbolDefinitions();
@@ -27427,6 +30016,10 @@ class ProcessWidgetComponent {
27427
30016
  b.targetType === 'symbolInstance' &&
27428
30017
  b.targetId === nestedInst.id) ?? [];
27429
30018
  for (const binding of passThroughBindings) {
30019
+ // A parent property without a value leaves the child property without one (AB#8022)
30020
+ if (binding.targetPropertyId && parentNoValue.has(binding.propertyId)) {
30021
+ nestedNoValue?.add(binding.targetPropertyId);
30022
+ }
27430
30023
  const sourceValue = parentPropertyValues[binding.propertyId];
27431
30024
  if (sourceValue !== undefined && binding.targetPropertyId) {
27432
30025
  // Evaluate expression if present
@@ -27511,7 +30104,7 @@ class ProcessWidgetComponent {
27511
30104
  * Gets property values for a symbol instance.
27512
30105
  * Combines default values with any values passed from parent bindings.
27513
30106
  */
27514
- getSymbolInstancePropertyValues(symbolInstance, definition) {
30107
+ getSymbolInstancePropertyValues(symbolInstance, definition, noValue) {
27515
30108
  const values = {};
27516
30109
  // Initialize with default values from transform properties
27517
30110
  for (const prop of definition.transformProperties ?? []) {
@@ -27519,16 +30112,26 @@ class ProcessWidgetComponent {
27519
30112
  }
27520
30113
  // Override with values from parent diagram bindings (via 'property' effect type)
27521
30114
  const diagram = this._diagramConfig();
27522
- const boundData = this._boundData();
27523
- if (diagram?.propertyBindings && boundData) {
30115
+ const boundData = this._boundDataByBinding();
30116
+ if (diagram?.propertyBindings) {
27524
30117
  // Get parent property values
27525
- 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);
27526
30120
  // Find bindings that pass values to this symbol instance
27527
30121
  const propertyBindings = diagram.propertyBindings.filter(b => b.targetType === 'symbolInstance' &&
27528
30122
  b.targetId === symbolInstance.id &&
27529
30123
  b.effectType === 'property' &&
27530
30124
  b.targetPropertyId);
27531
30125
  for (const binding of propertyBindings) {
30126
+ // A parent property without a value leaves the symbol property without one,
30127
+ // so a text.value binding inside the symbol shows "–" (AB#8022)
30128
+ if (parentNoValue.has(binding.propertyId)) {
30129
+ noValue?.add(binding.targetPropertyId);
30130
+ }
30131
+ // Without bound data the symbol keeps its defaults, as before
30132
+ if (boundData.size === 0) {
30133
+ continue;
30134
+ }
27532
30135
  const parentValue = parentPropertyValues[binding.propertyId];
27533
30136
  if (parentValue === undefined) {
27534
30137
  continue;
@@ -27949,7 +30552,7 @@ class ProcessWidgetComponent {
27949
30552
  * Maps diagram exposed properties to values from entity attributes or query columns.
27950
30553
  * Applies optional expressions for value transformation/normalization.
27951
30554
  */
27952
- buildPropertyValuesFromBoundData(transformProperties, mappings, boundData) {
30555
+ buildPropertyValuesFromBoundData(transformProperties, mappings, boundDataByBinding, noValue) {
27953
30556
  const values = {};
27954
30557
  // Initialize with default values from transform properties
27955
30558
  for (const prop of transformProperties) {
@@ -27960,14 +30563,42 @@ class ProcessWidgetComponent {
27960
30563
  const prop = transformProperties.find(p => p.id === mapping.propertyId);
27961
30564
  if (!prop)
27962
30565
  continue;
30566
+ // Each mapping reads from its own binding (AB#8023). A pending binding has
30567
+ // no entry and leaves the default; an unknown name is reported once.
30568
+ const boundData = boundDataByBinding.get(mappingBindingName(mapping));
30569
+ if (!boundData) {
30570
+ // No data yet: a text.value binding shows "–", other effects keep the default
30571
+ noValue?.add(mapping.propertyId);
30572
+ continue;
30573
+ }
27963
30574
  let rawValue = undefined;
27964
30575
  if (mapping.sourceType === 'attribute' && boundData.type === 'entity' && boundData.entity) {
27965
30576
  // Extract value from entity attribute
27966
30577
  rawValue = this.extractAttributeValue(boundData.entity, mapping.sourcePath);
27967
30578
  }
27968
30579
  else if (mapping.sourceType === 'column' && boundData.type === 'query' && boundData.queryResult) {
27969
- // Extract value from query result (first row)
27970
- 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);
27971
30602
  }
27972
30603
  if (rawValue !== undefined) {
27973
30604
  let finalValue = rawValue;
@@ -27991,6 +30622,11 @@ class ProcessWidgetComponent {
27991
30622
  console.warn(`[ProcessWidget] Expression error for ${mapping.propertyId}:`, err);
27992
30623
  }
27993
30624
  }
30625
+ // A number property whose value is not numeric has no value for a text
30626
+ // (coerceToType turns it into 0, which a text must never show)
30627
+ if (prop.type === 'number' && !isNumericValue(finalValue)) {
30628
+ noValue?.add(mapping.propertyId);
30629
+ }
27994
30630
  // Convert to expected type
27995
30631
  values[mapping.propertyId] = this.coerceToType(finalValue, prop.type);
27996
30632
  }
@@ -28041,17 +30677,6 @@ class ProcessWidgetComponent {
28041
30677
  }
28042
30678
  return value;
28043
30679
  }
28044
- /**
28045
- * Extracts a column value from query results (first row).
28046
- */
28047
- extractQueryColumnValue(queryResult, columnName) {
28048
- if (!queryResult.rows || queryResult.rows.length === 0)
28049
- return undefined;
28050
- const firstRow = queryResult.rows[0];
28051
- // Try both the column name and sanitized version (dots -> underscores)
28052
- const value = firstRow.cells.get(columnName) ?? firstRow.cells.get(columnName.replace(/\./g, '_'));
28053
- return value;
28054
- }
28055
30680
  /**
28056
30681
  * Coerces a value to the expected type.
28057
30682
  */
@@ -28083,7 +30708,7 @@ class ProcessWidgetComponent {
28083
30708
  * Applies property bindings to primitives.
28084
30709
  * Creates clones of primitives with binding effects applied.
28085
30710
  */
28086
- applyBindingsToPrimitives(primitives, bindings, propertyValues) {
30711
+ applyBindingsToPrimitives(primitives, bindings, propertyValues, properties, noValue) {
28087
30712
  return primitives.map(primitive => {
28088
30713
  // Find bindings for this primitive
28089
30714
  const primitiveBindings = bindings.filter(b => b.targetType === 'primitive' && b.targetId === primitive.id);
@@ -28094,6 +30719,17 @@ class ProcessWidgetComponent {
28094
30719
  let modified = this.clonePrimitive(primitive);
28095
30720
  for (const binding of primitiveBindings) {
28096
30721
  const propertyValue = propertyValues[binding.propertyId];
30722
+ if (binding.effectType === 'text.value') {
30723
+ // Formatted live value; "–" without a value, never 0 or the static text (AB#8022)
30724
+ const input = noValue?.has(binding.propertyId) ? undefined : propertyValue;
30725
+ const text = evaluateTextValue(input, binding, {
30726
+ evaluator: this.expressionEvaluator,
30727
+ locale: this.hostLanguage() ?? WIDGET_NUMBER_LOCALE,
30728
+ property: properties?.find(p => p.id === binding.propertyId)
30729
+ });
30730
+ modified = applyTextValueToPrimitive(modified, text);
30731
+ continue;
30732
+ }
28097
30733
  if (propertyValue === undefined)
28098
30734
  continue;
28099
30735
  // Build expression context
@@ -29200,8 +31836,14 @@ class ProcessWidgetComponent {
29200
31836
  <span>No process diagram configured</span>
29201
31837
  </div>
29202
31838
  }
31839
+ @if (!isLoading() && !error() && bindingErrors().length > 0) {
31840
+ <div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
31841
+ <span class="binding-error-icon" aria-hidden="true">⚠</span>
31842
+ <span class="binding-error-text">{{ bindingErrorText() }}</span>
31843
+ </div>
31844
+ }
29203
31845
  </div>
29204
- `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
31846
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.Eager });
29205
31847
  }
29206
31848
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: ProcessWidgetComponent, decorators: [{
29207
31849
  type: Component,
@@ -30201,14 +32843,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
30201
32843
  <span>No process diagram configured</span>
30202
32844
  </div>
30203
32845
  }
32846
+ @if (!isLoading() && !error() && bindingErrors().length > 0) {
32847
+ <div class="binding-error-badge" role="status" [attr.title]="bindingErrorText()">
32848
+ <span class="binding-error-icon" aria-hidden="true">⚠</span>
32849
+ <span class="binding-error-text">{{ bindingErrorText() }}</span>
32850
+ </div>
32851
+ }
30204
32852
  </div>
30205
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
32853
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:block;width:100%;height:100%}.process-widget{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;overflow:hidden;background-color:var(--process-bg, #fafafa)}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;color:var(--kendo-color-subtle, #666)}.loading-spinner{width:40px;height:40px;border:3px solid #e0e0e0;border-top-color:var(--kendo-color-primary, #1976d2);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:1rem;color:var(--kendo-color-error, #d32f2f)}.error-icon{font-size:2rem}.error-message{font-size:.875rem;text-align:center}.no-config{display:flex;align-items:center;justify-content:center;color:var(--kendo-color-subtle, #999);font-style:italic}.diagram-container{position:relative;overflow:hidden}.binding-error-badge{position:absolute;top:4px;right:4px;max-width:calc(100% - 8px);display:flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:.75rem;color:var(--kendo-color-error, #d32f2f);background-color:var(--kendo-color-surface-alt, #ffffff);border:1px solid var(--kendo-color-error, #d32f2f);z-index:1}.binding-error-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.diagram-container.fit-to-bounds{width:100%!important;height:100%!important}.process-svg{display:block}.diagram-container.fit-to-bounds .process-svg{width:100%;height:100%}.canvas-background{fill:var(--process-canvas-bg, #fafafa)}.element{cursor:default}.element.interactive{cursor:pointer}.element.interactive:hover{filter:brightness(1.1)}.tank-fill{transition:height .5s ease-out,y .5s ease-out}.tank-label{pointer-events:none;-webkit-user-select:none;user-select:none}.pump-element .rotating{animation:rotate 1s linear infinite;transform-origin:center}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.blinking{animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.connection-flow{animation:flow 2s linear infinite}@keyframes flow{0%{stroke-dashoffset:0}to{stroke-dashoffset:-40}}.digital-display-value{font-family:Courier New,monospace;letter-spacing:2px}\n"] }]
30206
32854
  }], propDecorators: { config: [{
30207
32855
  type: Input
30208
32856
  }], containerRef: [{
30209
32857
  type: ViewChild,
30210
32858
  args: ['containerRef']
30211
32859
  }] } });
32860
+ /** Whether a mapped value is a finite number or a numeric string (AB#8022). */
32861
+ function isNumericValue(value) {
32862
+ if (typeof value === 'number') {
32863
+ return Number.isFinite(value);
32864
+ }
32865
+ if (typeof value === 'string' && value.trim() !== '') {
32866
+ return Number.isFinite(Number(value.trim()));
32867
+ }
32868
+ return false;
32869
+ }
30212
32870
 
30213
32871
  /**
30214
32872
  * Configuration dialog for Process Widget.
@@ -31412,6 +34070,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
31412
34070
  * - Import and call registerProcessWidget() only in routes/components that need it
31413
34071
  * - Or use provideProcessWidget() in app config for eager loading
31414
34072
  */
34073
+ /**
34074
+ * The legacy single-binding fields of one binding named `default` (AB#8023):
34075
+ * a widget with exactly that one binding is saved in the legacy shape, so no
34076
+ * existing board is migrated silently.
34077
+ */
34078
+ function legacyFieldsOf(binding) {
34079
+ return {
34080
+ dataBindingMode: binding.mode,
34081
+ bindingCkTypeId: binding.ckTypeId,
34082
+ bindingRtId: binding.rtId,
34083
+ bindingQueryRtId: binding.queryRtId,
34084
+ bindingQueryName: binding.queryName,
34085
+ bindingQueryFamily: binding.queryFamily,
34086
+ bindingIgnoreTimeFilter: binding.ignoreTimeFilter,
34087
+ bindingEntitySelectorId: binding.entitySelectorId,
34088
+ bindingFilters: binding.filters
34089
+ };
34090
+ }
34091
+ /**
34092
+ * Normalizes the binding fields before persisting: a single binding named
34093
+ * `default` collapses into the legacy fields; anything else keeps `bindings`.
34094
+ */
34095
+ function normalizeBindings(widget) {
34096
+ if (!hasNamedProcessBindings(widget)) {
34097
+ return widget;
34098
+ }
34099
+ const bindings = widget.bindings;
34100
+ if (bindings.length === 1 && bindings[0].name === DEFAULT_PROCESS_BINDING_NAME) {
34101
+ const { bindings: _dropped, ...rest } = widget;
34102
+ return { ...rest, ...legacyFieldsOf(bindings[0]) };
34103
+ }
34104
+ return widget;
34105
+ }
31415
34106
  /**
31416
34107
  * Registers the Process Widget with the widget registry.
31417
34108
  *
@@ -31467,7 +34158,19 @@ function registerProcessWidget(registry) {
31467
34158
  initialBindingFilters: widget.bindingFilters,
31468
34159
  initialPropertyMappings: widget.propertyMappings
31469
34160
  }),
31470
- 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
+ } : ({
31471
34174
  ...widget,
31472
34175
  // Diagram selection
31473
34176
  processDiagramRtId: result.processDiagramRtId,
@@ -31507,35 +34210,42 @@ function registerProcessWidget(registry) {
31507
34210
  initialZoom: 1
31508
34211
  }),
31509
34212
  // SOLID: Serialization for persistence
31510
- toPersistedConfig: (widget) => ({
31511
- dataSourceType: 'static',
31512
- config: {
31513
- // Diagram selection
31514
- processDiagramRtId: widget.processDiagramRtId,
31515
- inlineConfig: widget.inlineConfig,
31516
- // Display options
31517
- fitToBounds: widget.fitToBounds,
31518
- allowZoom: widget.allowZoom,
31519
- allowPan: widget.allowPan,
31520
- showToolbar: widget.showToolbar,
31521
- initialZoom: widget.initialZoom,
31522
- // Data binding
31523
- dataBindingMode: widget.dataBindingMode,
31524
- bindingCkTypeId: widget.bindingCkTypeId,
31525
- bindingRtId: widget.bindingRtId,
31526
- bindingQueryRtId: widget.bindingQueryRtId,
31527
- bindingQueryName: widget.bindingQueryName,
31528
- // Stream-data binding — written only when set.
31529
- ...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
31530
- ...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
31531
- ...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
31532
- bindingFilters: widget.bindingFilters,
31533
- propertyMappings: widget.propertyMappings
31534
- }
31535
- }),
34213
+ toPersistedConfig: (input) => {
34214
+ const widget = normalizeBindings(input);
34215
+ return {
34216
+ dataSourceType: 'static',
34217
+ config: {
34218
+ // Diagram selection
34219
+ processDiagramRtId: widget.processDiagramRtId,
34220
+ inlineConfig: widget.inlineConfig,
34221
+ // Display options
34222
+ fitToBounds: widget.fitToBounds,
34223
+ allowZoom: widget.allowZoom,
34224
+ allowPan: widget.allowPan,
34225
+ showToolbar: widget.showToolbar,
34226
+ initialZoom: widget.initialZoom,
34227
+ // Data binding
34228
+ dataBindingMode: widget.dataBindingMode,
34229
+ bindingCkTypeId: widget.bindingCkTypeId,
34230
+ bindingRtId: widget.bindingRtId,
34231
+ bindingQueryRtId: widget.bindingQueryRtId,
34232
+ bindingQueryName: widget.bindingQueryName,
34233
+ // Stream-data binding — written only when set.
34234
+ ...(widget.bindingQueryFamily && { bindingQueryFamily: widget.bindingQueryFamily }),
34235
+ ...(widget.bindingIgnoreTimeFilter && { bindingIgnoreTimeFilter: true }),
34236
+ ...(widget.bindingEntitySelectorId && { bindingEntitySelectorId: widget.bindingEntitySelectorId }),
34237
+ bindingFilters: widget.bindingFilters,
34238
+ propertyMappings: widget.propertyMappings,
34239
+ // Named bindings (AB#8023) — written only when present, so legacy
34240
+ // widgets encode byte-identically.
34241
+ ...(hasNamedProcessBindings(widget) && { bindings: toPersistedProcessBindings(widget.bindings) })
34242
+ }
34243
+ };
34244
+ },
31536
34245
  // SOLID: Deserialization from persistence
31537
34246
  fromPersistedConfig: (data, base) => {
31538
34247
  const config = parseConfig(data);
34248
+ const bindings = parsePersistedProcessBindings(config['bindings']);
31539
34249
  return {
31540
34250
  ...base,
31541
34251
  rtId: data.rtId,
@@ -31563,7 +34273,9 @@ function registerProcessWidget(registry) {
31563
34273
  bindingIgnoreTimeFilter: config['bindingIgnoreTimeFilter'],
31564
34274
  bindingEntitySelectorId: config['bindingEntitySelectorId'],
31565
34275
  bindingFilters: config['bindingFilters'],
31566
- propertyMappings: config['propertyMappings']
34276
+ propertyMappings: config['propertyMappings'],
34277
+ // Named bindings (AB#8023); absent key stays absent.
34278
+ ...(bindings && { bindings })
31567
34279
  };
31568
34280
  }
31569
34281
  });
@@ -32704,7 +35416,9 @@ function registerDefaultWidgets(registry) {
32704
35416
  initialShowDataLabels: barWidget.showDataLabels,
32705
35417
  initialColorThresholds: barWidget.colorThresholds,
32706
35418
  initialDefaultBarColor: barWidget.defaultBarColor,
32707
- initialFilters: barWidget.filters
35419
+ initialFilters: barWidget.filters,
35420
+ initialAdditionalSeries: barWidget.additionalSeries,
35421
+ initialPrimarySeries: barWidget.primarySeries
32708
35422
  };
32709
35423
  },
32710
35424
  applyConfigResult: (widget, result) => {
@@ -32735,7 +35449,9 @@ function registerDefaultWidgets(registry) {
32735
35449
  showDataLabels: result.showDataLabels,
32736
35450
  colorThresholds: result.colorThresholds,
32737
35451
  defaultBarColor: result.defaultBarColor,
32738
- filters
35452
+ filters,
35453
+ additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
35454
+ primarySeries: barPrimaryOrUndefined(result.primarySeries)
32739
35455
  };
32740
35456
  },
32741
35457
  // SOLID: Factory function
@@ -32769,6 +35485,9 @@ function registerDefaultWidgets(registry) {
32769
35485
  valueScale: widget.valueScale,
32770
35486
  colorThresholds: widget.colorThresholds,
32771
35487
  defaultBarColor: widget.defaultBarColor,
35488
+ // AB#7953: written only when set, so a single-query bar chart encodes exactly as before.
35489
+ ...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
35490
+ ...(barPrimaryOrUndefined(widget.primarySeries) && { primarySeries: barPrimaryOrUndefined(widget.primarySeries) }),
32772
35491
  queryName: widget.dataSource.queryName,
32773
35492
  queryRtId: widget.dataSource.queryRtId,
32774
35493
  ...(widget.dataSource.queryFamily && {
@@ -32804,7 +35523,8 @@ function registerDefaultWidgets(registry) {
32804
35523
  valueScale: config['valueScale'],
32805
35524
  colorThresholds: config['colorThresholds'],
32806
35525
  defaultBarColor: config['defaultBarColor'],
32807
- filters: config['filters']
35526
+ filters: config['filters'],
35527
+ ...barChartSeriesFromPersisted(config)
32808
35528
  };
32809
35529
  }
32810
35530
  });
@@ -32840,7 +35560,10 @@ function registerDefaultWidgets(registry) {
32840
35560
  initialShowMarkers: lineWidget.showMarkers,
32841
35561
  initialShowDataBadge: lineWidget.showDataBadge,
32842
35562
  initialReferenceLines: lineWidget.referenceLines,
32843
- initialFilters: lineWidget.filters
35563
+ initialFilters: lineWidget.filters,
35564
+ initialAdditionalSeries: lineWidget.additionalSeries,
35565
+ initialStacked: lineWidget.stacked,
35566
+ initialPrimarySeries: lineWidget.primarySeries
32844
35567
  };
32845
35568
  },
32846
35569
  applyConfigResult: (widget, result) => {
@@ -32876,7 +35599,10 @@ function registerDefaultWidgets(registry) {
32876
35599
  showMarkers: result.showMarkers,
32877
35600
  showDataBadge: result.showDataBadge,
32878
35601
  referenceLines: result.referenceLines,
32879
- filters
35602
+ filters,
35603
+ additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
35604
+ stacked: result.stacked || undefined,
35605
+ primarySeries: displayOptionsOrUndefined(result.primarySeries)
32880
35606
  };
32881
35607
  },
32882
35608
  createDefaultConfig: (base) => ({
@@ -32927,7 +35653,12 @@ function registerDefaultWidgets(registry) {
32927
35653
  ...(widget.dataSource.aggregateSeriesByGroup && {
32928
35654
  aggregateSeriesByGroup: true
32929
35655
  }),
32930
- filters: widget.filters
35656
+ filters: widget.filters,
35657
+ // AB#7951: written only when set, so a single-query chart encodes exactly as before.
35658
+ ...(widget.additionalSeries?.length && { additionalSeries: widget.additionalSeries }),
35659
+ ...(widget.stacked && { stacked: true }),
35660
+ // AB#7952: written only when a primary display option is not default.
35661
+ ...(displayOptionsOrUndefined(widget.primarySeries) && { primarySeries: displayOptionsOrUndefined(widget.primarySeries) })
32931
35662
  }
32932
35663
  }),
32933
35664
  fromPersistedConfig: (data, base) => {
@@ -32948,7 +35679,8 @@ function registerDefaultWidgets(registry) {
32948
35679
  showMarkers: config['showMarkers'] ?? false,
32949
35680
  showDataBadge: config['showDataBadge'] ?? true,
32950
35681
  referenceLines: config['referenceLines'],
32951
- filters: config['filters']
35682
+ filters: config['filters'],
35683
+ ...lineChartSeriesFromPersisted(config)
32952
35684
  };
32953
35685
  }
32954
35686
  });
@@ -33984,6 +36716,72 @@ function provideWidgetRegistrations(registrations) {
33984
36716
  })
33985
36717
  ]);
33986
36718
  }
36719
+ /**
36720
+ * Reads `additionalSeries` / `stacked` / `primarySeries` of a persisted line chart (AB#7951,
36721
+ * AB#7952). All keys are absent on a single-query chart, so the result is `{}` and the loaded config
36722
+ * has no such keys. Kept entries: query series with a key and a data source, formula series with a
36723
+ * key and a formula string (AB#7952). Entries of an unknown `kind` are dropped (a newer library may
36724
+ * store kinds this version does not know). The display options `visible` / `stacked` are kept only
36725
+ * as `false`.
36726
+ */
36727
+ function lineChartSeriesFromPersisted(config) {
36728
+ const out = {};
36729
+ const raw = config['additionalSeries'];
36730
+ if (Array.isArray(raw)) {
36731
+ const series = raw.filter((s) => {
36732
+ if (!s || typeof s !== 'object')
36733
+ return false;
36734
+ const entry = s;
36735
+ if (typeof entry.key !== 'string')
36736
+ return false;
36737
+ if (entry.kind === 'query')
36738
+ return !!entry.dataSource && typeof entry.dataSource.queryRtId === 'string';
36739
+ if (entry.kind === 'formula')
36740
+ return typeof entry.formula === 'string';
36741
+ return false;
36742
+ });
36743
+ if (series.length > 0)
36744
+ out.additionalSeries = series;
36745
+ }
36746
+ if (config['stacked'] === true)
36747
+ out.stacked = true;
36748
+ const primary = config['primarySeries'];
36749
+ if (primary && typeof primary === 'object') {
36750
+ const options = displayOptionsOrUndefined(primary);
36751
+ if (options)
36752
+ out.primarySeries = options;
36753
+ }
36754
+ return out;
36755
+ }
36756
+ /**
36757
+ * Reads `additionalSeries` / `primarySeries` of a persisted bar chart (AB#7953) with the line chart
36758
+ * rules (unknown kinds dropped, display options only as `false`). The bar chart keeps only
36759
+ * `visible` of the primary options. Both keys are absent on a single-query bar chart.
36760
+ */
36761
+ function barChartSeriesFromPersisted(config) {
36762
+ const line = lineChartSeriesFromPersisted({ additionalSeries: config['additionalSeries'], primarySeries: config['primarySeries'] });
36763
+ const out = {};
36764
+ if (line.additionalSeries)
36765
+ out.additionalSeries = line.additionalSeries;
36766
+ const primary = barPrimaryOrUndefined(line.primarySeries);
36767
+ if (primary)
36768
+ out.primarySeries = primary;
36769
+ return out;
36770
+ }
36771
+ /** Bar chart primary options: `{ visible: false }` or `undefined`. */
36772
+ function barPrimaryOrUndefined(options) {
36773
+ return options?.visible === false ? { visible: false } : undefined;
36774
+ }
36775
+ /** Display options reduced to their non-default (`false`) values; `undefined` when all are default. */
36776
+ function displayOptionsOrUndefined(options) {
36777
+ if (!options)
36778
+ return undefined;
36779
+ const out = {
36780
+ ...(options.visible === false && { visible: false }),
36781
+ ...(options.stacked === false && { stacked: false })
36782
+ };
36783
+ return Object.keys(out).length > 0 ? out : undefined;
36784
+ }
33987
36785
 
33988
36786
  /** True when the selector is configured to follow another selector. */
33989
36787
  function isFollowerSelector(selector) {
@@ -43188,5 +45986,5 @@ function provideCockpitWidgets() {
43188
45986
  * Generated bundle index. Do not edit.
43189
45987
  */
43190
45988
 
43191
- export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, blueprintUpdatesKpi, buildHourlyHistogram, categoryStatus, chartSeriesColors, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, versionKpi, withMeshBoardNavigation };
45989
+ export { ADAPTER_OFFLINE_GRACE_MS, ASSOCIATION_HOP_SOURCE_BATCH, ASSOCIATION_HOP_TARGETS_PER_SOURCE, ATTENTION_SEVERITY_ORDER, AdaptersAttentionProvider, AddWidgetDialogComponent, AiInsightsConfigDialogComponent, AiInsightsService, AiInsightsWidgetComponent, AlertBannerConfigDialogComponent, AlertBannerWidgetComponent, AlertListConfigDialogComponent, AlertListWidgetComponent, AssociationsConfigDialogComponent, AttentionListConfigDialogComponent, AttentionListWidgetComponent, AutoRefreshTimerService, BUILT_IN_ATTENTION_PROVIDERS, BarChartConfigDialogComponent, BarChartWidgetComponent, BlueprintUpdatesAttentionProvider, COCKPIT_ADAPTER_LIMIT, COCKPIT_ATTENTION_PROVIDERS, COCKPIT_BLUEPRINT_CATALOG_LIMIT, COCKPIT_CHILD_TENANT_IDS, COCKPIT_DATA_FLOW_LIMIT, COCKPIT_EXPLAIN_HANDLER, COCKPIT_FAILED_EXECUTIONS_OPTIONS, COCKPIT_KPI_GATES, COCKPIT_KPI_SLOW_LOADING_MS, COCKPIT_LINK_RESOLVER, COCKPIT_RECENT_ITEMS, COCKPIT_ROLES, COCKPIT_SERVICE_NAMES, COCKPIT_VERSION_SOURCE, COCKPIT_VIEWER_ACCESS, COCKPIT_WIDGET_MESSAGES, COCKPIT_WIDGET_TYPES, CkModelsResolveFailedAttentionProvider, CockpitAdapterStatesService, CockpitAttentionService, CockpitBlueprintStatusService, CockpitChildTenantsService, CockpitCkModelStatesService, CockpitContextService, CockpitDataFlowExecutionsService, CockpitKpiConfigDialogComponent, CockpitKpiService, CockpitKpiWidgetComponent, CockpitServiceHealthService, DAY_MS, DEFAULT_ATTENTION_MAX_ITEMS, DEFAULT_COCKPIT_WIDGET_MESSAGES, DEFAULT_FAILED_EXECUTIONS_OPTIONS, DEFAULT_MESHBOARD_MESSAGES, DEFAULT_RECENT_ITEMS_MAX, DEFAULT_TIME_ZONE_MODE, DashboardDataService, DashboardGridService, EditModeStateService, EditWidgetDialogComponent, EntityAssociationsWidgetComponent, EntityCardConfigDialogComponent, EntityCardWidgetComponent, EntityDetailDialogComponent, EntitySelectorEditorComponent, EntitySelectorScopePathEditorComponent, EntitySelectorToolbarComponent, FALLBACK_SERIES_COLORS, FailedExecutionsAttentionProvider, FeaturesNotInstalledAttentionProvider, FormulaFieldComponent, GaugeConfigDialogComponent, GaugeWidgetComponent, HISTOGRAM_HOURS, HOUR_MS, HeatmapConfigDialogComponent, HeatmapWidgetComponent, KPI_ERROR_TEXT, KpiConfigDialogComponent, KpiWidgetComponent, LazyMarkdownComponent, LineChartConfigDialogComponent, LineChartSeriesEditorComponent, LineChartWidgetComponent, MAX_RECENT_ITEMS, MAX_SCOPE_HOPS, MESHBOARD_CHROME_ROUTE_DATA, MESHBOARD_DESCRIPTION_MARKER, MESHBOARD_EDIT_QUERY_PARAM, MESHBOARD_HEADER_MODE_ROUTE_DATA, MESHBOARD_LABEL_RESOLVER, MESHBOARD_LANGUAGE, MESHBOARD_MESSAGES, MESHBOARD_OPTIONS, MESHBOARD_TENANT_ID_PROVIDER, MarkdownConfigDialogComponent, MarkdownWidgetComponent, MeshBoardDataService, MeshBoardGridService, MeshBoardManagerDialogComponent, MeshBoardPersistenceService, MeshBoardSettingsDialogComponent, MeshBoardSettingsResult, MeshBoardStateService, MeshBoardViewComponent, NOT_AVAILABLE_TEXT, PieChartConfigDialogComponent, PieChartWidgetComponent, QueryExecutorService, QuerySelectorComponent, RecentItemsConfigDialogComponent, RecentItemsWidgetComponent, RuntimeEntitySelectorComponent, SERVICE_MANAGED_BLUEPRINT_PREFIX, STATE_STATUS_BY_KEY, ServiceHealthConfigDialogComponent, ServiceHealthWidgetComponent, ServicesHealthAttentionProvider, StatsGridConfigDialogComponent, StatsGridWidgetComponent, StatusIndicatorConfigDialogComponent, StatusIndicatorWidgetComponent, StatusListConfigDialogComponent, StatusListWidgetComponent, SummaryCardConfigDialogComponent, SummaryCardWidgetComponent, TableConfigDialogComponent, TableWidgetComponent, TableWidgetDataSourceDirective, UnregisteredPoolsAttentionProvider, WidgetFactoryService, WidgetGroupComponent, WidgetGroupConfigDialogComponent, WidgetNotConfiguredComponent, WidgetRegistryService, adapterKpi, adaptersInError, adaptersOffline, alignOnTimeGrid, blueprintUpdatesKpi, bucketStepMs, buildHourlyHistogram, categoryStatus, chartSeriesColors, circularReferenceMessage, ckModelKpi, classifyQuery, compareBlueprintVersions, compareMeshBoardNavigation, countFlowExecutions, countLabel, countPipelineExecutions, emptyQueryExecutionResult, enabledButNotInstalled, evaluateSeriesFormulas, executionKpi, failedExecutionsSeverity, formatCockpitMessage, formatCount, humanizeCategory, isAdapterAtRest, isAdapterExpectedToRun, isAdapterOnline, isCockpitWidgetType, isMeshBoardChrome, isMeshBoardHeaderMode, isServiceConfigured, joinEncodedDescription, kpiKindOf, latestStatisticsUpdate, legacyCockpitLinkTarget, loadNgxMarkdown, nameList, newMeshBoardConfig, normalizeNavigation, observeThemeChanges, orderSeriesFormulas, parseHostTimeRange, pickLocalizedText, pipelineExecutionInputs, plural, provideCockpitWidgetHost, provideCockpitWidgets, provideDefaultWidgets, provideMeshBoard, provideMeshBoardLanguage, provideMeshBoardMessages, provideProcessWidget, provideWidgetRegistrations, queryFamily, readMeshBoardNavigation, recentRelativeTime, registerCockpitWidgets, registerDefaultWidgets, registerProcessWidget, resolveChartColor, resolveCockpitWidgetMessages, resolveMeshBoardChrome, resolveMeshBoardHeaderMode, resolveMeshBoardLabel, resolveMeshBoardMessages, selectProviders, seriesFormulaOperands, serviceStateText, servicesHealthKpi, sortAttentionFindings, sparklineGeometry, splitBlueprintId, splitEncodedDescription, statusColor, summarizeAdapterOnline, tenantCountKpi, toBlueprintStatus, toServiceHealthStatus, truncationNote, validateSeriesFormulas, versionKpi, withMeshBoardNavigation };
43192
45990
  //# sourceMappingURL=meshmakers-octo-meshboard.mjs.map