@meshmakers/octo-meshboard 3.4.290 → 3.4.320

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.
@@ -2884,6 +2884,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
2884
2884
  }]
2885
2885
  }], ctorParameters: () => [{ type: i1.Apollo }] });
2886
2886
 
2887
+ const ResolveSeriesQueryDocumentDto = gql `
2888
+ query resolveSeriesQuery($input: ResolveSeriesQueryInput!) {
2889
+ streamData {
2890
+ resolveSeriesQuery(input: $input) {
2891
+ archiveRtId
2892
+ effectiveBucketMs
2893
+ points
2894
+ reducingFunction
2895
+ signal
2896
+ actualPoints
2897
+ diagnostic
2898
+ }
2899
+ }
2900
+ }
2901
+ `;
2902
+ class ResolveSeriesQueryDtoGQL extends i1.Query {
2903
+ document = ResolveSeriesQueryDocumentDto;
2904
+ constructor(apollo) {
2905
+ super(apollo);
2906
+ }
2907
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ResolveSeriesQueryDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
2908
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ResolveSeriesQueryDtoGQL, providedIn: 'root' });
2909
+ }
2910
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ResolveSeriesQueryDtoGQL, decorators: [{
2911
+ type: Injectable,
2912
+ args: [{
2913
+ providedIn: 'root'
2914
+ }]
2915
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
2916
+
2887
2917
  const EMPTY_RESULT_BASE = Object.freeze({
2888
2918
  queryRtId: null,
2889
2919
  associatedCkTypeId: null,
@@ -2905,6 +2935,7 @@ class QueryExecutorService {
2905
2935
  runtimeGql = inject(ExecuteRuntimeQueryDtoGQL);
2906
2936
  streamDataGql = inject(ExecuteStreamDataQueryDtoGQL);
2907
2937
  persistentQueriesGql = inject(GetSystemPersistentQueriesDtoGQL);
2938
+ resolveSeriesGql = inject(ResolveSeriesQueryDtoGQL);
2908
2939
  /**
2909
2940
  * Cache of resolved query families, keyed by query rtId. Filled lazily for
2910
2941
  * legacy widget configs that pre-date the `queryFamily` field — saves a
@@ -3006,6 +3037,29 @@ class QueryExecutorService {
3006
3037
  };
3007
3038
  }));
3008
3039
  }
3040
+ /**
3041
+ * Resolution-aware series routing (AB#4290): asks the backend which archive/rollup to
3042
+ * query for a base archive family, time window and target point count — so a widget can
3043
+ * render ~`targetPoints` points without knowing which physical archive holds the data at a
3044
+ * usable grain. The caller then runs {@link executeStreamData} (downsampling) against the
3045
+ * returned `archiveRtId` with `limit = points`. Returns null when StreamData is not enabled.
3046
+ */
3047
+ async resolveSeriesQuery(input) {
3048
+ const result = await firstValueFrom(this.resolveSeriesGql.fetch({ variables: { input }, fetchPolicy: 'network-only' }));
3049
+ const decision = result.data?.streamData?.resolveSeriesQuery;
3050
+ if (!decision) {
3051
+ return null;
3052
+ }
3053
+ return {
3054
+ archiveRtId: String(decision.archiveRtId),
3055
+ effectiveBucketMs: Number(decision.effectiveBucketMs),
3056
+ points: decision.points,
3057
+ reducingFunction: decision.reducingFunction,
3058
+ signal: decision.signal,
3059
+ actualPoints: decision.actualPoints ?? null,
3060
+ diagnostic: decision.diagnostic ?? null
3061
+ };
3062
+ }
3009
3063
  buildStreamDataArg(args) {
3010
3064
  // Skip the entire `arg` field when the caller has nothing to override —
3011
3065
  // the persisted query then runs with its intrinsic from/to/limit and the
@@ -4847,6 +4901,36 @@ function matchesAttributePath(cellPath, configField) {
4847
4901
  const cellBase = stripAggregationFunctionSuffix(cellPath);
4848
4902
  return toCanonicalAttributeKey(cellBase) === toCanonicalAttributeKey(configField);
4849
4903
  }
4904
+ /**
4905
+ * Finds the single cell in a row that a config field refers to, preferring an
4906
+ * exact `sanitizeFieldName` match over the loose canonical fallback of
4907
+ * {@link matchesAttributePath}.
4908
+ *
4909
+ * This disambiguation matters for grouping-aggregation results, where a group-by
4910
+ * cell (`state`) coexists with an aggregation cell over the same attribute
4911
+ * (`state_count`). A bare category config (`state`) loose-matches BOTH via the
4912
+ * canonical fallback; iterating cells and assigning on every match let the later
4913
+ * `state_count` cell overwrite the category with the raw count, so the pie/bar
4914
+ * legend showed the count instead of the enum label (AB#4293). Resolving each
4915
+ * field to its best cell — exact wins — keeps the group-by and aggregation cells
4916
+ * on their intended fields while still honouring the loose fallback for legacy
4917
+ * configs whose only matching cell is the wire-form aggregation column.
4918
+ */
4919
+ function findCellForField(cells, configField) {
4920
+ if (!configField)
4921
+ return undefined;
4922
+ let looseMatch;
4923
+ for (const cell of cells) {
4924
+ if (!cell?.attributePath)
4925
+ continue;
4926
+ if (sanitizeFieldName(cell.attributePath) === configField)
4927
+ return cell; // exact wins
4928
+ if (looseMatch === undefined && matchesAttributePath(cell.attributePath, configField)) {
4929
+ looseMatch = cell;
4930
+ }
4931
+ }
4932
+ return looseMatch;
4933
+ }
4850
4934
  /**
4851
4935
  * Parses a value to a number.
4852
4936
  * Returns 0 for NaN or non-numeric values.
@@ -4876,12 +4960,9 @@ function extractAggregationValue(queryResult, valueField) {
4876
4960
  const cells = firstRow.cells?.items ?? [];
4877
4961
  // Find the value field if specified
4878
4962
  if (valueField) {
4879
- for (const cell of cells) {
4880
- if (!cell?.attributePath)
4881
- continue;
4882
- if (matchesAttributePath(cell.attributePath, valueField)) {
4883
- return parseNumericValue(cell.value);
4884
- }
4963
+ const cell = findCellForField(cells, valueField);
4964
+ if (cell) {
4965
+ return parseNumericValue(cell.value);
4885
4966
  }
4886
4967
  }
4887
4968
  // Fallback: return first cell value
@@ -4908,20 +4989,10 @@ function extractGroupedAggregationValue(queryResult, categoryField, categoryValu
4908
4989
  if (!row || !SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4909
4990
  continue;
4910
4991
  const cells = row.cells?.items ?? [];
4911
- let categoryMatch = false;
4912
- let value = 0;
4913
- for (const cell of cells) {
4914
- if (!cell?.attributePath)
4915
- continue;
4916
- if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
4917
- categoryMatch = true;
4918
- }
4919
- if (matchesAttributePath(cell.attributePath, valueField)) {
4920
- value = parseNumericValue(cell.value);
4921
- }
4922
- }
4923
- if (categoryMatch) {
4924
- return value;
4992
+ const categoryCell = findCellForField(cells, categoryField);
4993
+ if (categoryCell && String(categoryCell.value) === categoryValue) {
4994
+ const valueCell = findCellForField(cells, valueField);
4995
+ return valueCell ? parseNumericValue(valueCell.value) : 0;
4925
4996
  }
4926
4997
  }
4927
4998
  return 0;
@@ -4945,19 +5016,13 @@ function processStaticSeriesData(rows, categoryField, seriesConfigs) {
4945
5016
  if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4946
5017
  continue;
4947
5018
  const cells = row.cells?.items ?? [];
4948
- let categoryValue = '';
5019
+ const categoryCell = findCellForField(cells, categoryField);
5020
+ const categoryValue = categoryCell ? String(categoryCell.value ?? '') : '';
4949
5021
  const rowValues = new Map();
4950
- for (const cell of cells) {
4951
- if (!cell?.attributePath)
4952
- continue;
4953
- if (matchesAttributePath(cell.attributePath, categoryField)) {
4954
- categoryValue = String(cell.value ?? '');
4955
- }
4956
- // Check if this cell is one of our series fields
4957
- for (const seriesConfig of seriesConfigs) {
4958
- if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
4959
- rowValues.set(seriesConfig.field, parseNumericValue(cell.value));
4960
- }
5022
+ for (const seriesConfig of seriesConfigs) {
5023
+ const seriesCell = findCellForField(cells, seriesConfig.field);
5024
+ if (seriesCell) {
5025
+ rowValues.set(seriesConfig.field, parseNumericValue(seriesCell.value));
4961
5026
  }
4962
5027
  }
4963
5028
  if (categoryValue !== '') {
@@ -4995,22 +5060,12 @@ function processDynamicSeriesData(rows, categoryField, seriesGroupField, valueFi
4995
5060
  if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
4996
5061
  continue;
4997
5062
  const cells = row.cells?.items ?? [];
4998
- let categoryValue = '';
4999
- let seriesGroupValue = '';
5000
- let numericValue = 0;
5001
- for (const cell of cells) {
5002
- if (!cell?.attributePath)
5003
- continue;
5004
- if (matchesAttributePath(cell.attributePath, categoryField)) {
5005
- categoryValue = String(cell.value ?? '');
5006
- }
5007
- else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
5008
- seriesGroupValue = String(cell.value ?? '');
5009
- }
5010
- else if (matchesAttributePath(cell.attributePath, valueField)) {
5011
- numericValue = parseNumericValue(cell.value);
5012
- }
5013
- }
5063
+ const categoryCell = findCellForField(cells, categoryField);
5064
+ const seriesGroupCell = findCellForField(cells, seriesGroupField);
5065
+ const valueCell = findCellForField(cells, valueField);
5066
+ const categoryValue = categoryCell ? String(categoryCell.value ?? '') : '';
5067
+ const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
5068
+ const numericValue = valueCell ? parseNumericValue(valueCell.value) : 0;
5014
5069
  if (categoryValue && seriesGroupValue) {
5015
5070
  allCategories.add(categoryValue);
5016
5071
  allSeriesGroups.add(seriesGroupValue);
@@ -5048,18 +5103,10 @@ function processPieChartData(rows, categoryField, valueField) {
5048
5103
  if (!SUPPORTED_ROW_TYPES$1.includes(row.__typename ?? ''))
5049
5104
  continue;
5050
5105
  const cells = row.cells?.items ?? [];
5051
- let categoryValue = '';
5052
- let numericValue = 0;
5053
- for (const cell of cells) {
5054
- if (!cell?.attributePath)
5055
- continue;
5056
- if (matchesAttributePath(cell.attributePath, categoryField)) {
5057
- categoryValue = String(cell.value ?? '');
5058
- }
5059
- else if (matchesAttributePath(cell.attributePath, valueField)) {
5060
- numericValue = parseNumericValue(cell.value);
5061
- }
5062
- }
5106
+ const categoryCell = findCellForField(cells, categoryField);
5107
+ const valueCell = findCellForField(cells, valueField);
5108
+ const categoryValue = categoryCell ? String(categoryCell.value ?? '') : '';
5109
+ const numericValue = valueCell ? parseNumericValue(valueCell.value) : 0;
5063
5110
  if (categoryValue) {
5064
5111
  result.push({
5065
5112
  category: categoryValue,
@@ -12001,16 +12048,16 @@ class PieChartWidgetComponent {
12001
12048
  const chartData = result.rows
12002
12049
  .filter(row => PieChartWidgetComponent.SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
12003
12050
  .map(row => {
12004
- let category = '';
12051
+ // Resolve each field to its best cell (exact match wins over the loose
12052
+ // aggregation-suffix fallback) so a `state` category is not overwritten
12053
+ // by the `state_count` aggregation cell — AB#4293.
12054
+ const categoryCell = findCellForField(row.cells, this.config.categoryField);
12055
+ const valueCell = findCellForField(row.cells, this.config.valueField);
12056
+ const category = categoryCell ? String(categoryCell.value ?? '') : '';
12005
12057
  let value = 0;
12006
- for (const cell of row.cells) {
12007
- if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
12008
- category = String(cell.value ?? '');
12009
- }
12010
- if (matchesAttributePath(cell.attributePath, this.config.valueField)) {
12011
- const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
12012
- value = isNaN(numValue) ? 0 : numValue;
12013
- }
12058
+ if (valueCell) {
12059
+ const numValue = typeof valueCell.value === 'number' ? valueCell.value : parseFloat(String(valueCell.value));
12060
+ value = isNaN(numValue) ? 0 : numValue;
12014
12061
  }
12015
12062
  return { category, value };
12016
12063
  })
@@ -13063,18 +13110,17 @@ class BarChartWidgetComponent {
13063
13110
  seriesMap.set(seriesConfig.field, []);
13064
13111
  }
13065
13112
  for (const row of filteredRows) {
13066
- let categoryValue = '';
13113
+ // Exact match wins over the loose aggregation-suffix fallback so a group-by
13114
+ // category is not overwritten by an aggregation cell over the same attribute
13115
+ // (e.g. `state` vs `state_count`) — AB#4293.
13116
+ const categoryCell = findCellForField(row.cells, this.config.categoryField);
13117
+ const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
13067
13118
  const rowValues = new Map();
13068
- for (const cell of row.cells) {
13069
- if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
13070
- categoryValue = this.formatCategoryValue(cell.value);
13071
- }
13072
- // Check if this cell is one of our series fields
13073
- for (const seriesConfig of (this.config.series ?? [])) {
13074
- if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
13075
- const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
13076
- rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
13077
- }
13119
+ for (const seriesConfig of (this.config.series ?? [])) {
13120
+ const seriesCell = findCellForField(row.cells, seriesConfig.field);
13121
+ if (seriesCell) {
13122
+ const numValue = typeof seriesCell.value === 'number' ? seriesCell.value : parseFloat(String(seriesCell.value));
13123
+ rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
13078
13124
  }
13079
13125
  }
13080
13126
  if (categoryValue !== '') {
@@ -13122,22 +13168,18 @@ class BarChartWidgetComponent {
13122
13168
  const allCategories = new Set();
13123
13169
  const allSeriesGroups = new Set();
13124
13170
  for (const row of filteredRows) {
13125
- let categoryValue = '';
13126
- let seriesGroupValue = '';
13171
+ // Exact match wins over the loose aggregation-suffix fallback (AB#4293).
13172
+ const categoryCell = findCellForField(row.cells, categoryField);
13173
+ const seriesGroupCell = findCellForField(row.cells, seriesGroupField);
13174
+ const valueCell = findCellForField(row.cells, valueField);
13175
+ const categoryValue = categoryCell ? this.formatCategoryValue(categoryCell.value) : '';
13176
+ const seriesGroupValue = seriesGroupCell ? String(seriesGroupCell.value ?? '') : '';
13127
13177
  let numericValue = 0;
13128
- for (const cell of row.cells) {
13129
- if (matchesAttributePath(cell.attributePath, categoryField)) {
13130
- categoryValue = this.formatCategoryValue(cell.value);
13131
- }
13132
- else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
13133
- seriesGroupValue = String(cell.value ?? '');
13134
- }
13135
- else if (matchesAttributePath(cell.attributePath, valueField)) {
13136
- const val = cell.value;
13137
- numericValue = typeof val === 'number' ? val : parseFloat(String(val));
13138
- if (isNaN(numericValue))
13139
- numericValue = 0;
13140
- }
13178
+ if (valueCell) {
13179
+ const val = valueCell.value;
13180
+ numericValue = typeof val === 'number' ? val : parseFloat(String(val));
13181
+ if (isNaN(numericValue))
13182
+ numericValue = 0;
13141
13183
  }
13142
13184
  if (categoryValue && seriesGroupValue) {
13143
13185
  allCategories.add(categoryValue);
@@ -17012,10 +17054,9 @@ function extractAggregation(result, valueField) {
17012
17054
  if (!firstRow)
17013
17055
  return 0;
17014
17056
  if (valueField) {
17015
- for (const cell of firstRow.cells) {
17016
- if (matchesAttributePath(cell.attributePath, valueField)) {
17017
- return parseNumericValue(cell.value);
17018
- }
17057
+ const cell = findCellForField(firstRow.cells, valueField);
17058
+ if (cell) {
17059
+ return parseNumericValue(cell.value);
17019
17060
  }
17020
17061
  }
17021
17062
  // Fallback: first cell value when no specific field is configured.
@@ -17027,18 +17068,11 @@ function extractGroupedAggregation(result, categoryField, categoryValue, valueFi
17027
17068
  for (const row of result.rows) {
17028
17069
  if (!SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
17029
17070
  continue;
17030
- let categoryMatch = false;
17031
- let value = 0;
17032
- for (const cell of row.cells) {
17033
- if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
17034
- categoryMatch = true;
17035
- }
17036
- if (matchesAttributePath(cell.attributePath, valueField)) {
17037
- value = parseNumericValue(cell.value);
17038
- }
17071
+ const categoryCell = findCellForField(row.cells, categoryField);
17072
+ if (categoryCell && String(categoryCell.value) === categoryValue) {
17073
+ const valueCell = findCellForField(row.cells, valueField);
17074
+ return valueCell ? parseNumericValue(valueCell.value) : 0;
17039
17075
  }
17040
- if (categoryMatch)
17041
- return value;
17042
17076
  }
17043
17077
  return 0;
17044
17078
  }