@meshmakers/octo-meshboard 3.4.280 → 3.4.310

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);
@@ -14420,6 +14462,11 @@ class LineChartWidgetComponent {
14420
14462
  const unit = this._seriesUnitMap().get(seriesName);
14421
14463
  return unit ? ` ${unit}` : '';
14422
14464
  }
14465
+ /** The downsampling min/max envelope is rendered as a separate `<name> (min/max)` rangeArea
14466
+ * series; it is excluded from the shared tooltip so only the actual value lines are listed. */
14467
+ isBandSeries(seriesName) {
14468
+ return seriesName.endsWith(' (min/max)');
14469
+ }
14423
14470
  async loadData() {
14424
14471
  if (this.isNotConfigured())
14425
14472
  return;
@@ -14736,18 +14783,29 @@ class LineChartWidgetComponent {
14736
14783
  [position]="config.legendPosition ?? 'right'">
14737
14784
  </kendo-chart-legend>
14738
14785
 
14739
- <kendo-chart-tooltip>
14740
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
14786
+ <!--
14787
+ Shared (category) tooltip: keyed on the category under the cursor's X, so the
14788
+ reported point always matches the hovered time position. A non-shared tooltip
14789
+ selects the nearest point by 2D distance, which makes the highlight jump
14790
+ horizontally to an unrelated category whenever the cursor is off the (often flat)
14791
+ line — see AB#4258. The min/max envelope (rangeArea) series is filtered out.
14792
+ -->
14793
+ <kendo-chart-tooltip [shared]="true">
14794
+ <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
14741
14795
  <div class="chart-tooltip">
14742
- <strong>{{ category }}</strong><br/>
14743
- {{ series.name }}: {{ formatValue(value) }}{{ getUnitForSeries(series.name) }}
14796
+ <strong>{{ category }}</strong>
14797
+ @for (point of points; track point.series.name) {
14798
+ @if (!isBandSeries(point.series.name)) {
14799
+ <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
14800
+ }
14801
+ }
14744
14802
  </div>
14745
14803
  </ng-template>
14746
14804
  </kendo-chart-tooltip>
14747
14805
  </kendo-chart>
14748
14806
  }
14749
14807
  </div>
14750
- `, 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}.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.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.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 });
14808
+ `, 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}.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 });
14751
14809
  }
14752
14810
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: LineChartWidgetComponent, decorators: [{
14753
14811
  type: Component,
@@ -14849,11 +14907,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14849
14907
  [position]="config.legendPosition ?? 'right'">
14850
14908
  </kendo-chart-legend>
14851
14909
 
14852
- <kendo-chart-tooltip>
14853
- <ng-template kendoChartSeriesTooltipTemplate let-value="value" let-category="category" let-series="series">
14910
+ <!--
14911
+ Shared (category) tooltip: keyed on the category under the cursor's X, so the
14912
+ reported point always matches the hovered time position. A non-shared tooltip
14913
+ selects the nearest point by 2D distance, which makes the highlight jump
14914
+ horizontally to an unrelated category whenever the cursor is off the (often flat)
14915
+ line — see AB#4258. The min/max envelope (rangeArea) series is filtered out.
14916
+ -->
14917
+ <kendo-chart-tooltip [shared]="true">
14918
+ <ng-template kendoChartSharedTooltipTemplate let-category="category" let-points="points">
14854
14919
  <div class="chart-tooltip">
14855
- <strong>{{ category }}</strong><br/>
14856
- {{ series.name }}: {{ formatValue(value) }}{{ getUnitForSeries(series.name) }}
14920
+ <strong>{{ category }}</strong>
14921
+ @for (point of points; track point.series.name) {
14922
+ @if (!isBandSeries(point.series.name)) {
14923
+ <br/>{{ point.series.name }}: {{ formatValue(point.value) }}{{ getUnitForSeries(point.series.name) }}
14924
+ }
14925
+ }
14857
14926
  </div>
14858
14927
  </ng-template>
14859
14928
  </kendo-chart-tooltip>
@@ -16985,10 +17054,9 @@ function extractAggregation(result, valueField) {
16985
17054
  if (!firstRow)
16986
17055
  return 0;
16987
17056
  if (valueField) {
16988
- for (const cell of firstRow.cells) {
16989
- if (matchesAttributePath(cell.attributePath, valueField)) {
16990
- return parseNumericValue(cell.value);
16991
- }
17057
+ const cell = findCellForField(firstRow.cells, valueField);
17058
+ if (cell) {
17059
+ return parseNumericValue(cell.value);
16992
17060
  }
16993
17061
  }
16994
17062
  // Fallback: first cell value when no specific field is configured.
@@ -17000,18 +17068,11 @@ function extractGroupedAggregation(result, categoryField, categoryValue, valueFi
17000
17068
  for (const row of result.rows) {
17001
17069
  if (!SUPPORTED_ROW_TYPES.has(row.__typename ?? ''))
17002
17070
  continue;
17003
- let categoryMatch = false;
17004
- let value = 0;
17005
- for (const cell of row.cells) {
17006
- if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
17007
- categoryMatch = true;
17008
- }
17009
- if (matchesAttributePath(cell.attributePath, valueField)) {
17010
- value = parseNumericValue(cell.value);
17011
- }
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;
17012
17075
  }
17013
- if (categoryMatch)
17014
- return value;
17015
17076
  }
17016
17077
  return 0;
17017
17078
  }