@meshmakers/octo-meshboard 3.5.580 → 3.5.590

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.
package/README.md CHANGED
@@ -587,6 +587,55 @@ interface BarChartWidgetConfig {
587
587
  }
588
588
  ```
589
589
 
590
+ ### Line Chart Widget
591
+ Displays time series as line or area charts; every value of `seriesGroupField` becomes one line.
592
+ Series from further queries, formula series and per-series options are described in the library's
593
+ `CLAUDE.md` (section *Line-chart resolution-aware fan-out* and the sections starting with *Line chart:* and
594
+ *Line and bar chart:*).
595
+
596
+ ```typescript
597
+ interface LineChartWidgetConfig {
598
+ type: 'lineChart';
599
+ chartType: 'line' | 'area';
600
+ categoryField: string; // date/time field of the X axis
601
+ seriesGroupField: string; // one line per value
602
+ valueField: string;
603
+ unitField?: string;
604
+ excludedSeriesGroupValues?: string[]; // series group values left out (see below)
605
+ // ... legend, markers, filters, additionalSeries, primarySeries, stacked, bucketMinutes
606
+ }
607
+ ```
608
+
609
+ **Leaving series out (`excludedSeriesGroupValues`).** A grouped chart can leave out chosen series of its
610
+ primary query, for example a register that duplicates another one. On the fan-out paths (resolution-aware charts, one line per source entity or per group) the values are not queried; in plain grouping the rows the query returns for them are dropped before the series are formed. Either way they are not drawn
611
+ and not in the legend, on the raw archive as on rollups (no query column is needed, so it works where a
612
+ row filter on the group column fails). Board JSON of a widget config:
613
+
614
+ ```json
615
+ {
616
+ "chartType": "line",
617
+ "categoryField": "window_start",
618
+ "seriesGroupField": "obisCode",
619
+ "valueField": "amount.value",
620
+ "resolutionAware": true,
621
+ "requiredAggregation": "SUM",
622
+ "aggregateSeriesByGroup": true,
623
+ "excludedSeriesGroupValues": ["1-1:2.9.0 G.03R"]
624
+ }
625
+ ```
626
+
627
+ - Matching is exact on the stored `seriesGroupField` value: case and blanks as stored, before any display
628
+ label. A value that does not occur changes nothing.
629
+ - A resolution-aware chart without an entity scope and without `aggregateSeriesByGroup` draws one merged
630
+ line. When an excluded value occurs among the source entities, that line covers only the source entities
631
+ of the other values (at most 5000 entities are read; entities without a value of the group field drop out).
632
+ - When every value is excluded, or the exclusion leaves no value, the chart shows its normal no-data state.
633
+ - The entity-selector scope still applies to the remaining series. Additional series are not affected
634
+ (hide them with `visible: false`).
635
+ - The key is written only when the list is not empty; charts without it load and save unchanged.
636
+ - In the dialog: *Field Mapping → Excluded series*, shown once a series group field is set. Choosing another
637
+ series group field or another query clears the list.
638
+
590
639
  ### Stats Grid Widget
591
640
  Displays multiple KPIs in a grid layout.
592
641
 
@@ -4530,8 +4530,10 @@ class QueryExecutorService {
4530
4530
  * Group-aggregation source resolution (AB#4714): fetches the source entities of `ckTypeId` in a
4531
4531
  * single bulk read and buckets their rtIds by the value of `groupField` (canonical match — case
4532
4532
  * and non-alphanumerics are ignored, so an archive column name maps to its CK attribute). Returns
4533
- * `groupValue → rtIds`. Optionally restricted to `restrictRtIds` (an entity-selector scope) —
4534
- * entities outside that set are dropped. Entities whose group attribute is missing/null are
4533
+ * `groupValue → rtIds`. Optionally restricted to `restrictRtIds` (an entity-selector scope): the
4534
+ * restriction is applied server-side as an `rtId IN` field filter of the same request (so the
4535
+ * {@link ENTITY_GROUP_FETCH_CAP} counts scoped entities only), and entities outside the set are
4536
+ * dropped again on the client as a guard. Entities whose group attribute is missing/null are
4535
4537
  * omitted (they cannot be attributed to a line). The population is bounded to
4536
4538
  * {@link ENTITY_GROUP_FETCH_CAP}; a truncated read is logged (never silently capped).
4537
4539
  */
@@ -4541,7 +4543,12 @@ class QueryExecutorService {
4541
4543
  const wanted = canon(groupField);
4542
4544
  const restrict = restrictRtIds && restrictRtIds.length > 0 ? new Set(restrictRtIds) : null;
4543
4545
  const attributeNames = await this.matchingAttributeNames(ckTypeId, wanted, canon);
4544
- const res = await firstValueFrom(this.entitiesGql.fetch({ variables: { ckTypeId, attributeNames, first: QueryExecutorService.ENTITY_GROUP_FETCH_CAP } }));
4546
+ const res = await firstValueFrom(this.entitiesGql.fetch({ variables: {
4547
+ ckTypeId,
4548
+ attributeNames,
4549
+ first: QueryExecutorService.ENTITY_GROUP_FETCH_CAP,
4550
+ fieldFilters: QueryExecutorService.withRtIdScope(undefined, restrictRtIds)
4551
+ } }));
4545
4552
  const connection = res.data?.runtime?.runtimeEntities;
4546
4553
  const items = connection?.items ?? [];
4547
4554
  const total = connection?.totalCount ?? items.length;
@@ -16795,6 +16802,32 @@ function rowValue(row, valueField, downsampled) {
16795
16802
  }
16796
16803
  return toNumberOrNull(findCellForField(row.cells, valueField)?.value);
16797
16804
  }
16805
+ /** Raw `seriesGroupField` value of a row: its cell, else the stream-data row property (`rtId`, AB#8069). */
16806
+ function rowSeriesGroupValue(row, seriesGroupField) {
16807
+ return findCellForField(row.cells, seriesGroupField)?.value ?? streamDataRowFieldValue(row, seriesGroupField);
16808
+ }
16809
+ /**
16810
+ * The rows whose raw `seriesGroupField` value is not excluded (AB#8128), matched exactly on the stored
16811
+ * value. Returns the rows unchanged when nothing is excluded.
16812
+ */
16813
+ function withoutExcludedSeriesGroups(rows, seriesGroupField, excluded) {
16814
+ if (excluded.size === 0)
16815
+ return rows;
16816
+ return rows.filter(row => {
16817
+ const group = rowSeriesGroupValue(row, seriesGroupField);
16818
+ return group === null || group === undefined || !excluded.has(String(group));
16819
+ });
16820
+ }
16821
+ /** The distinct non-empty raw `seriesGroupField` values of the rows, sorted (AB#8128: values offered for exclusion). */
16822
+ function distinctSeriesGroupValues(rows, seriesGroupField) {
16823
+ const values = new Set();
16824
+ for (const row of rows) {
16825
+ const group = rowSeriesGroupValue(row, seriesGroupField);
16826
+ if (group !== null && group !== undefined && String(group) !== '')
16827
+ values.add(String(group));
16828
+ }
16829
+ return [...values].sort();
16830
+ }
16798
16831
  /**
16799
16832
  * Rows of the primary source → one series per `seriesGroupField` value, exactly the grouping of
16800
16833
  * the single-query chart (a later row of the same series and bucket wins).
@@ -16806,7 +16839,7 @@ function primaryRowsToSeries(rows, fields, downsampled) {
16806
16839
  if (instant === null)
16807
16840
  continue;
16808
16841
  // Stream-data rows carry the source entity as row property (`rtId`), not as cell (AB#8069).
16809
- const group = findCellForField(row.cells, fields.seriesGroupField)?.value ?? streamDataRowFieldValue(row, fields.seriesGroupField);
16842
+ const group = rowSeriesGroupValue(row, fields.seriesGroupField);
16810
16843
  if (group === null || group === undefined || String(group) === '')
16811
16844
  continue;
16812
16845
  const name = String(group);
@@ -17188,6 +17221,11 @@ function uniqueLabels(labels) {
17188
17221
 
17189
17222
  /** Name of the second value axis of a bar chart with a line on it (AB#8046). */
17190
17223
  const SECONDARY_AXIS = 'secondary';
17224
+ /**
17225
+ * Name of the main value axis (AB#8234). Every series names its axis explicitly: binding '' to an
17226
+ * unnamed main axis detaches the series from it (axis stays 0-1.2, bars drawn outside the plot).
17227
+ */
17228
+ const PRIMARY_AXIS = 'primary';
17191
17229
  const CHART_TYPE_MAPPING = {
17192
17230
  column: { type: 'column', stack: false },
17193
17231
  bar: { type: 'bar', stack: false },
@@ -17232,6 +17270,8 @@ class BarChartWidgetComponent {
17232
17270
  isMultiSource = this._multiSource.asReadonly();
17233
17271
  data = computed(() => this._seriesData(), /* @ts-ignore */
17234
17272
  ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
17273
+ /** Name of the main value axis (template binding, AB#8234). */
17274
+ get primaryAxisName() { return PRIMARY_AXIS; }
17235
17275
  /** Name of the second value axis (template binding, AB#8046). */
17236
17276
  get secondaryAxisName() { return SECONDARY_AXIS; }
17237
17277
  /** A drawn line series uses the second value axis (AB#8046). */
@@ -17772,6 +17812,7 @@ class BarChartWidgetComponent {
17772
17812
 
17773
17813
  <kendo-chart-value-axis>
17774
17814
  <kendo-chart-value-axis-item
17815
+ [name]="primaryAxisName"
17775
17816
  [line]="{ visible: false }"
17776
17817
  [labels]="{ color: chartTheme().muted }"
17777
17818
  [majorGridLines]="{ color: chartTheme().grid }">
@@ -17798,7 +17839,7 @@ class BarChartWidgetComponent {
17798
17839
  [colorField]="hasColorThresholds() && !series.asLine ? '_color' : ''"
17799
17840
  [border]="{ width: 0 }"
17800
17841
  [gap]="0.8"
17801
- [axis]="series.secondaryAxis ? secondaryAxisName : ''"
17842
+ [axis]="series.secondaryAxis ? secondaryAxisName : primaryAxisName"
17802
17843
  [dashType]="series.dashType ?? 'solid'"
17803
17844
  [missingValues]="'gap'"
17804
17845
  [markers]="{ visible: !!series.asLine }"
@@ -17893,6 +17934,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17893
17934
 
17894
17935
  <kendo-chart-value-axis>
17895
17936
  <kendo-chart-value-axis-item
17937
+ [name]="primaryAxisName"
17896
17938
  [line]="{ visible: false }"
17897
17939
  [labels]="{ color: chartTheme().muted }"
17898
17940
  [majorGridLines]="{ color: chartTheme().grid }">
@@ -17919,7 +17961,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
17919
17961
  [colorField]="hasColorThresholds() && !series.asLine ? '_color' : ''"
17920
17962
  [border]="{ width: 0 }"
17921
17963
  [gap]="0.8"
17922
- [axis]="series.secondaryAxis ? secondaryAxisName : ''"
17964
+ [axis]="series.secondaryAxis ? secondaryAxisName : primaryAxisName"
17923
17965
  [dashType]="series.dashType ?? 'solid'"
17924
17966
  [missingValues]="'gap'"
17925
17967
  [markers]="{ visible: !!series.asLine }"
@@ -20323,7 +20365,7 @@ class LineChartWidgetComponent {
20323
20365
  console.error('Error loading Line Chart data:', err);
20324
20366
  throw err;
20325
20367
  })));
20326
- const filteredRows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
20368
+ const filteredRows = withoutExcludedSeriesGroups(result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? '')), this.config.seriesGroupField, this.excludedSeriesGroups());
20327
20369
  const downsampled = streamDataArgs?.queryMode === QueryModeDto.DownsamplingDto;
20328
20370
  this.processData(filteredRows, downsampled);
20329
20371
  this._dataInfo.set({ rows: filteredRows.length, points: this._categories().length, total: result.totalCount });
@@ -20408,30 +20450,57 @@ class LineChartWidgetComponent {
20408
20450
  * Group-aggregation fan-out (AB#4714): buckets the source entities by `seriesGroupField` and runs
20409
20451
  * one downsampling call per group with the group's rtIds. The transient downsampling reduces over
20410
20452
  * the group's members server-side, yielding one aggregate line per distinct group value. Falls
20411
- * back to a single merged series when no group can be resolved.
20453
+ * back to a single merged series when no group can be resolved. Excluded series group values
20454
+ * (AB#8128) are not queried; when they remove every group, nothing is queried and there is no data.
20412
20455
  */
20413
20456
  async loadGroupAggregatedRows(ckTypeId, sourceRtIds, downsample) {
20414
- const groups = await this.queryExecutor.fetchEntityGroups(ckTypeId, this.config.seriesGroupField, sourceRtIds);
20415
- if (groups.size === 0) {
20457
+ const allGroups = await this.queryExecutor.fetchEntityGroups(ckTypeId, this.config.seriesGroupField, sourceRtIds);
20458
+ if (allGroups.size === 0) {
20416
20459
  // No group attribute resolved → a single merged line over the whole (optionally scoped) set.
20417
20460
  return downsample(sourceRtIds, this.config.seriesGroupField);
20418
20461
  }
20419
- const perGroup = await Promise.all(Array.from(groups.entries()).map(([groupValue, rtIds]) => downsample(rtIds, groupValue)));
20462
+ const excluded = this.excludedSeriesGroups();
20463
+ const groups = Array.from(allGroups.entries()).filter(([groupValue]) => !excluded.has(groupValue));
20464
+ if (groups.length === 0)
20465
+ return [];
20466
+ const perGroup = await Promise.all(groups.map(([groupValue, rtIds]) => downsample(rtIds, groupValue)));
20420
20467
  return perGroup.flat();
20421
20468
  }
20422
20469
  /**
20423
20470
  * Default resolution-aware fan-out (AB#4290): one downsampling call per source rtId so each
20424
20471
  * register stays its own line, labeled from its `seriesGroupField` attribute. With no source
20425
- * scope a single merged series is produced (labeled by the series-group field name).
20472
+ * scope a single merged series is produced (labeled by the series-group field name); when an
20473
+ * excluded series group value (AB#8128) occurs among the source entities, that series covers only
20474
+ * the source entities of the other values. Scoped sources whose label is excluded are not queried.
20475
+ * On that unscoped path the merged series is built from the `fetchEntityGroups` read, so entities
20476
+ * without a group value and entities beyond its 5000 cap are not in it (a full read measured 16-80 s
20477
+ * per load, and the downsampling query fails above ~10 000 rtIds).
20426
20478
  */
20427
20479
  async loadPerRtIdRows(ckTypeId, sourceRtIds, downsample) {
20428
- const seriesRtIds = sourceRtIds && sourceRtIds.length > 0 ? sourceRtIds : [undefined];
20480
+ const excluded = this.excludedSeriesGroups();
20481
+ if (!sourceRtIds || sourceRtIds.length === 0) {
20482
+ if (excluded.size === 0 || !ckTypeId)
20483
+ return downsample(undefined, this.config.seriesGroupField);
20484
+ const groups = await this.queryExecutor.fetchEntityGroups(ckTypeId, this.config.seriesGroupField);
20485
+ if (!Array.from(groups.keys()).some(groupValue => excluded.has(groupValue))) {
20486
+ return downsample(undefined, this.config.seriesGroupField);
20487
+ }
20488
+ const remaining = Array.from(groups.entries()).filter(([groupValue]) => !excluded.has(groupValue)).flatMap(([, rtIds]) => rtIds);
20489
+ return remaining.length > 0 ? downsample(remaining, this.config.seriesGroupField) : [];
20490
+ }
20429
20491
  const labels = ckTypeId
20430
- ? await this.queryExecutor.fetchSeriesLabels(ckTypeId, sourceRtIds ?? [], this.config.seriesGroupField)
20492
+ ? await this.queryExecutor.fetchSeriesLabels(ckTypeId, sourceRtIds, this.config.seriesGroupField)
20431
20493
  : new Map();
20432
- const perSeries = await Promise.all(seriesRtIds.map(rtId => downsample(rtId ? [rtId] : undefined, rtId ? (labels.get(rtId) ?? rtId) : this.config.seriesGroupField)));
20494
+ const perSeries = await Promise.all(sourceRtIds
20495
+ .map(rtId => [rtId, labels.get(rtId) ?? rtId])
20496
+ .filter(([, label]) => !excluded.has(label))
20497
+ .map(([rtId, label]) => downsample([rtId], label)));
20433
20498
  return perSeries.flat();
20434
20499
  }
20500
+ /** Raw series group values the chart leaves out (AB#8128). */
20501
+ excludedSeriesGroups() {
20502
+ return new Set(this.config.excludedSeriesGroupValues ?? []);
20503
+ }
20435
20504
  /**
20436
20505
  * Reshapes one downsampling bin onto the widget's configured value + series-group fields so the
20437
20506
  * shared {@link processData} path plots it. The transient row carries the reduced value under a
@@ -20703,7 +20772,10 @@ class LineChartWidgetComponent {
20703
20772
  scopeRtIds: scope.kind === 'scoped' ? scope.rtIds : undefined
20704
20773
  }));
20705
20774
  const rows = result.rows.filter(row => LineChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''));
20706
- return { rows, downsampled };
20775
+ return {
20776
+ rows: source.primary ? withoutExcludedSeriesGroups(rows, this.config.seriesGroupField, this.excludedSeriesGroups()) : rows,
20777
+ downsampled
20778
+ };
20707
20779
  }
20708
20780
  /** Writes aligned series into the chart signals (multi-source path, AB#7951). */
20709
20781
  applyAlignedGrid(instants, series) {
@@ -21320,6 +21392,7 @@ class LineChartConfigDialogComponent {
21320
21392
  initialStacked;
21321
21393
  initialPrimarySeries;
21322
21394
  initialBucketMinutes;
21395
+ initialExcludedSeriesGroupValues;
21323
21396
  /** Options of the bucket size dropdown (AB#8046): automatic (`null`) plus the fixed sizes. */
21324
21397
  get bucketSizeOptions() {
21325
21398
  return [
@@ -21331,6 +21404,21 @@ class LineChartConfigDialogComponent {
21331
21404
  additionalSeries = [];
21332
21405
  /** Display options of the primary query's series (AB#7952). */
21333
21406
  primarySeries;
21407
+ /** Raw series group values left out of the chart (AB#8128). */
21408
+ excludedSeriesGroupValues = [];
21409
+ /** Values of the series group field offered for exclusion (AB#8128). */
21410
+ seriesGroupValues = [];
21411
+ isLoadingSeriesGroupValues = false;
21412
+ /** The offered values could not be loaded; typed values still work. */
21413
+ seriesGroupValuesFailed = false;
21414
+ seriesGroupValuesLoadToken = 0;
21415
+ /** Text typed into the Excluded series field; narrows the offered values. */
21416
+ seriesGroupValueFilter = '';
21417
+ /** The offered values that contain the typed text (case-insensitive). */
21418
+ get filteredSeriesGroupValues() {
21419
+ const filter = this.seriesGroupValueFilter.trim().toLowerCase();
21420
+ return filter ? this.seriesGroupValues.filter(v => v.toLowerCase().includes(filter)) : this.seriesGroupValues;
21421
+ }
21334
21422
  // State
21335
21423
  isLoadingInitial = false;
21336
21424
  isLoadingColumns = false;
@@ -21426,6 +21514,7 @@ class LineChartConfigDialogComponent {
21426
21514
  this.form.bucketMinutes = this.initialBucketMinutes && LINE_CHART_BUCKET_MINUTES.includes(this.initialBucketMinutes) ? this.initialBucketMinutes : null;
21427
21515
  this.additionalSeries = this.initialAdditionalSeries ? [...this.initialAdditionalSeries] : [];
21428
21516
  this.primarySeries = this.initialPrimarySeries ? { ...this.initialPrimarySeries } : undefined;
21517
+ this.excludedSeriesGroupValues = this.initialExcludedSeriesGroupValues ? [...this.initialExcludedSeriesGroupValues] : [];
21429
21518
  // Initialize filters
21430
21519
  if (this.initialFilters && this.initialFilters.length > 0) {
21431
21520
  this.filters = this.initialFilters.map((f, index) => ({
@@ -21444,6 +21533,7 @@ class LineChartConfigDialogComponent {
21444
21533
  if (query) {
21445
21534
  this.selectedPersistentQuery = query;
21446
21535
  await this.loadQueryColumns(query.rtId);
21536
+ await this.loadSeriesGroupValues();
21447
21537
  }
21448
21538
  }
21449
21539
  this.isLoadingInitial = false;
@@ -21460,6 +21550,11 @@ class LineChartConfigDialogComponent {
21460
21550
  this.form.seriesGroupField = '';
21461
21551
  this.form.valueField = '';
21462
21552
  this.form.unitField = '';
21553
+ this.excludedSeriesGroupValues = [];
21554
+ this.seriesGroupValues = [];
21555
+ this.seriesGroupValuesFailed = false;
21556
+ this.isLoadingSeriesGroupValues = false;
21557
+ this.seriesGroupValuesLoadToken++;
21463
21558
  if (query) {
21464
21559
  await this.loadQueryColumns(query.rtId);
21465
21560
  }
@@ -21518,6 +21613,54 @@ class LineChartConfigDialogComponent {
21518
21613
  aggregationType: c.aggregationType ?? null
21519
21614
  })));
21520
21615
  }
21616
+ /**
21617
+ * Loads the values of the series group field offered for exclusion (AB#8128): for a stream-data query the
21618
+ * group values of the source entities (the `fetchEntityGroups` read the widget groups by), for a runtime
21619
+ * query the distinct values of that column in the query result.
21620
+ */
21621
+ async loadSeriesGroupValues() {
21622
+ const query = this.selectedPersistentQuery;
21623
+ const field = this.form.seriesGroupField;
21624
+ const token = ++this.seriesGroupValuesLoadToken;
21625
+ this.seriesGroupValues = [];
21626
+ this.seriesGroupValuesFailed = false;
21627
+ if (!query || !field) {
21628
+ this.isLoadingSeriesGroupValues = false;
21629
+ return;
21630
+ }
21631
+ this.isLoadingSeriesGroupValues = true;
21632
+ const isCurrent = () => token === this.seriesGroupValuesLoadToken;
21633
+ try {
21634
+ const family = queryFamily(query.ckTypeId) ?? this.initialQueryFamily ?? await this.queryExecutor.resolveFamily(query.rtId);
21635
+ let values;
21636
+ if (family === 'streamData') {
21637
+ const ckTypeId = (await this.queryExecutor.fetchQueryArchive(query.rtId))?.ckTypeId;
21638
+ if (!ckTypeId)
21639
+ throw new Error('The query archive has no CK type.');
21640
+ values = [...(await this.queryExecutor.fetchEntityGroups(ckTypeId, field)).keys()].sort();
21641
+ }
21642
+ else {
21643
+ const result = await firstValueFrom(this.queryExecutor.execute(family, query.rtId));
21644
+ values = distinctSeriesGroupValues(result.rows, field);
21645
+ }
21646
+ if (isCurrent())
21647
+ this.seriesGroupValues = values;
21648
+ }
21649
+ catch (error) {
21650
+ console.error('Error loading series group values:', error);
21651
+ if (isCurrent())
21652
+ this.seriesGroupValuesFailed = true;
21653
+ }
21654
+ finally {
21655
+ if (isCurrent())
21656
+ this.isLoadingSeriesGroupValues = false;
21657
+ }
21658
+ }
21659
+ /** The author picked another series group field: the values excluded for the old field no longer apply. */
21660
+ async onSeriesGroupFieldChange() {
21661
+ this.excludedSeriesGroupValues = [];
21662
+ await this.loadSeriesGroupValues();
21663
+ }
21521
21664
  onFiltersChange(updatedFilters) {
21522
21665
  this.filters = updatedFilters;
21523
21666
  }
@@ -21556,7 +21699,8 @@ class LineChartConfigDialogComponent {
21556
21699
  additionalSeries: this.effectiveAdditionalSeries.length > 0 ? this.effectiveAdditionalSeries : undefined,
21557
21700
  stacked: this.form.chartType === 'area' && this.form.stacked ? true : undefined,
21558
21701
  primarySeries: this.selectedQueryFamily === 'streamData' ? this.primarySeries : undefined,
21559
- bucketMinutes: this.selectedQueryFamily === 'streamData' && this.form.bucketMinutes ? this.form.bucketMinutes : undefined
21702
+ bucketMinutes: this.selectedQueryFamily === 'streamData' && this.form.bucketMinutes ? this.form.bucketMinutes : undefined,
21703
+ excludedSeriesGroupValues: this.excludedSeriesGroupValues.length > 0 ? [...this.excludedSeriesGroupValues] : undefined
21560
21704
  };
21561
21705
  this.windowRef.close(result);
21562
21706
  }
@@ -21570,7 +21714,7 @@ class LineChartConfigDialogComponent {
21570
21714
  this.windowRef.close();
21571
21715
  }
21572
21716
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21573
- 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", initialBucketMinutes: "initialBucketMinutes" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
21717
+ 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", initialBucketMinutes: "initialBucketMinutes", initialExcludedSeriesGroupValues: "initialExcludedSeriesGroupValues" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
21574
21718
  <div class="config-container">
21575
21719
 
21576
21720
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -21659,6 +21803,7 @@ class LineChartConfigDialogComponent {
21659
21803
  [valueField]="'attributePath'"
21660
21804
  [valuePrimitive]="true"
21661
21805
  [(ngModel)]="form.seriesGroupField"
21806
+ (ngModelChange)="onSeriesGroupFieldChange()"
21662
21807
  placeholder="Select grouping field...">
21663
21808
  <ng-template kendoComboBoxItemTemplate let-dataItem>
21664
21809
  <div class="column-item">
@@ -21670,6 +21815,28 @@ class LineChartConfigDialogComponent {
21670
21815
  <p class="field-hint">Each unique value becomes a separate line (e.g., OBIS code).</p>
21671
21816
  </div>
21672
21817
 
21818
+ @if (form.seriesGroupField) {
21819
+ <div class="form-field">
21820
+ <label for="line-chart-excluded-series">Excluded series</label>
21821
+ <kendo-multiselect
21822
+ focusableId="line-chart-excluded-series"
21823
+ data-chart-option="excludedSeriesGroupValues"
21824
+ [data]="filteredSeriesGroupValues"
21825
+ [allowCustom]="true"
21826
+ [filterable]="true"
21827
+ (filterChange)="seriesGroupValueFilter = $event"
21828
+ [loading]="isLoadingSeriesGroupValues"
21829
+ [(ngModel)]="excludedSeriesGroupValues"
21830
+ placeholder="No series excluded - pick or type a value">
21831
+ </kendo-multiselect>
21832
+ @if (seriesGroupValuesFailed) {
21833
+ <p class="field-hint" data-hint="series-group-values-failed">Could not load the values of {{ form.seriesGroupField }} - type a value and press Enter.</p>
21834
+ } @else {
21835
+ <p class="field-hint">Values of {{ form.seriesGroupField }} whose line is left out. Exact match on the stored value.</p>
21836
+ }
21837
+ </div>
21838
+ }
21839
+
21673
21840
  <div class="form-field">
21674
21841
  <label>Value Field (Y-Axis) <span class="required">*</span></label>
21675
21842
  <kendo-combobox
@@ -21858,7 +22025,7 @@ class LineChartConfigDialogComponent {
21858
22025
  </button>
21859
22026
  </div>
21860
22027
  </div>
21861
- `, 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", "areaChart", "linesAllowed", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
22028
+ `, 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: "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", "areaChart", "linesAllowed", "defaultCategoryField"], outputs: ["primaryChange", "seriesChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
21862
22029
  }
21863
22030
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: LineChartConfigDialogComponent, decorators: [{
21864
22031
  type: Component,
@@ -21964,6 +22131,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
21964
22131
  [valueField]="'attributePath'"
21965
22132
  [valuePrimitive]="true"
21966
22133
  [(ngModel)]="form.seriesGroupField"
22134
+ (ngModelChange)="onSeriesGroupFieldChange()"
21967
22135
  placeholder="Select grouping field...">
21968
22136
  <ng-template kendoComboBoxItemTemplate let-dataItem>
21969
22137
  <div class="column-item">
@@ -21975,6 +22143,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
21975
22143
  <p class="field-hint">Each unique value becomes a separate line (e.g., OBIS code).</p>
21976
22144
  </div>
21977
22145
 
22146
+ @if (form.seriesGroupField) {
22147
+ <div class="form-field">
22148
+ <label for="line-chart-excluded-series">Excluded series</label>
22149
+ <kendo-multiselect
22150
+ focusableId="line-chart-excluded-series"
22151
+ data-chart-option="excludedSeriesGroupValues"
22152
+ [data]="filteredSeriesGroupValues"
22153
+ [allowCustom]="true"
22154
+ [filterable]="true"
22155
+ (filterChange)="seriesGroupValueFilter = $event"
22156
+ [loading]="isLoadingSeriesGroupValues"
22157
+ [(ngModel)]="excludedSeriesGroupValues"
22158
+ placeholder="No series excluded - pick or type a value">
22159
+ </kendo-multiselect>
22160
+ @if (seriesGroupValuesFailed) {
22161
+ <p class="field-hint" data-hint="series-group-values-failed">Could not load the values of {{ form.seriesGroupField }} - type a value and press Enter.</p>
22162
+ } @else {
22163
+ <p class="field-hint">Values of {{ form.seriesGroupField }} whose line is left out. Exact match on the stored value.</p>
22164
+ }
22165
+ </div>
22166
+ }
22167
+
21978
22168
  <div class="form-field">
21979
22169
  <label>Value Field (Y-Axis) <span class="required">*</span></label>
21980
22170
  <kendo-combobox
@@ -22211,6 +22401,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
22211
22401
  type: Input
22212
22402
  }], initialBucketMinutes: [{
22213
22403
  type: Input
22404
+ }], initialExcludedSeriesGroupValues: [{
22405
+ type: Input
22214
22406
  }] } });
22215
22407
 
22216
22408
  /**
@@ -37741,7 +37933,8 @@ function registerDefaultWidgets(registry) {
37741
37933
  initialAdditionalSeries: lineWidget.additionalSeries,
37742
37934
  initialStacked: lineWidget.stacked,
37743
37935
  initialPrimarySeries: lineWidget.primarySeries,
37744
- initialBucketMinutes: lineWidget.bucketMinutes
37936
+ initialBucketMinutes: lineWidget.bucketMinutes,
37937
+ initialExcludedSeriesGroupValues: lineWidget.excludedSeriesGroupValues
37745
37938
  };
37746
37939
  },
37747
37940
  applyConfigResult: (widget, result) => {
@@ -37781,7 +37974,8 @@ function registerDefaultWidgets(registry) {
37781
37974
  additionalSeries: result.additionalSeries?.length ? result.additionalSeries : undefined,
37782
37975
  stacked: result.stacked || undefined,
37783
37976
  primarySeries: displayOptionsOrUndefined(result.primarySeries),
37784
- bucketMinutes: bucketMinutesOrUndefined(result.bucketMinutes)
37977
+ bucketMinutes: bucketMinutesOrUndefined(result.bucketMinutes),
37978
+ excludedSeriesGroupValues: excludedSeriesGroupValuesOrUndefined(result.excludedSeriesGroupValues)
37785
37979
  };
37786
37980
  },
37787
37981
  createDefaultConfig: (base) => ({
@@ -37839,7 +38033,10 @@ function registerDefaultWidgets(registry) {
37839
38033
  // AB#7952: written only when a primary display option is not default.
37840
38034
  ...(displayOptionsOrUndefined(widget.primarySeries) && { primarySeries: displayOptionsOrUndefined(widget.primarySeries) }),
37841
38035
  // AB#8046: written only when a fixed bucket size is set.
37842
- ...(bucketMinutesOrUndefined(widget.bucketMinutes) && { bucketMinutes: bucketMinutesOrUndefined(widget.bucketMinutes) })
38036
+ ...(bucketMinutesOrUndefined(widget.bucketMinutes) && { bucketMinutes: bucketMinutesOrUndefined(widget.bucketMinutes) }),
38037
+ ...(excludedSeriesGroupValuesOrUndefined(widget.excludedSeriesGroupValues) && {
38038
+ excludedSeriesGroupValues: excludedSeriesGroupValuesOrUndefined(widget.excludedSeriesGroupValues)
38039
+ })
37843
38040
  }
37844
38041
  }),
37845
38042
  fromPersistedConfig: (data, base) => {
@@ -38935,12 +39132,22 @@ function lineChartSeriesFromPersisted(config) {
38935
39132
  const bucketMinutes = bucketMinutesOrUndefined(config['bucketMinutes']);
38936
39133
  if (bucketMinutes)
38937
39134
  out.bucketMinutes = bucketMinutes;
39135
+ const excluded = excludedSeriesGroupValuesOrUndefined(config['excludedSeriesGroupValues']);
39136
+ if (excluded)
39137
+ out.excludedSeriesGroupValues = excluded;
38938
39138
  return out;
38939
39139
  }
38940
39140
  /** A supported fixed bucket size of a line chart (AB#8046), else `undefined` (pixel-driven buckets). */
38941
39141
  function bucketMinutesOrUndefined(value) {
38942
39142
  return typeof value === 'number' && LINE_CHART_BUCKET_MINUTES.includes(value) ? value : undefined;
38943
39143
  }
39144
+ /** The excluded series group values of a line chart (AB#8128): the non-empty strings, without duplicates, else `undefined`. */
39145
+ function excludedSeriesGroupValuesOrUndefined(value) {
39146
+ if (!Array.isArray(value))
39147
+ return undefined;
39148
+ const values = [...new Set(value.filter((v) => typeof v === 'string' && v !== ''))];
39149
+ return values.length > 0 ? values : undefined;
39150
+ }
38944
39151
  /**
38945
39152
  * Reads `additionalSeries` / `primarySeries` of a persisted bar chart (AB#7953) with the line chart
38946
39153
  * rules (unknown kinds dropped, display options only as `false`). The bar chart keeps only