@meshmakers/octo-meshboard 3.4.260 → 3.4.280

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.
@@ -15577,6 +15577,15 @@ const COLOR_SCHEMES = {
15577
15577
  blue: (min, max) => buildGradientRanges(min, max, ['#e3f2fd', '#90caf9', '#42a5f5', '#1565c0', '#0d47a1']),
15578
15578
  heat: (min, max) => buildGradientRanges(min, max, ['#fff9c4', '#ffcc02', '#ff9800', '#f44336', '#b71c1c'])
15579
15579
  };
15580
+ /**
15581
+ * Fixed band colors for the `threshold` color mode: a cell equal to the target
15582
+ * is "ok" (green), below it "warn" (amber), above it "high" (red).
15583
+ */
15584
+ const THRESHOLD_COLORS = Object.freeze({
15585
+ ok: '#2e7d32',
15586
+ warn: '#ffb300',
15587
+ high: '#c62828'
15588
+ });
15580
15589
  function buildGradientRanges(min, max, colors) {
15581
15590
  if (max <= min) {
15582
15591
  return [{ from: min, to: min + 1, color: colors[0] }];
@@ -15927,6 +15936,17 @@ class HeatmapWidgetComponent {
15927
15936
  * Assigns colors to heatmap data items based on value range and color scheme.
15928
15937
  */
15929
15938
  assignColors(data) {
15939
+ // Threshold mode: absolute ok/warn/high bands around an expected target.
15940
+ // Falls back to gradient when no target can be resolved (so the widget never
15941
+ // renders blank just because no scope/target is configured).
15942
+ if ((this.config?.colorMode ?? 'gradient') === 'threshold') {
15943
+ const target = this.resolveThresholdTarget();
15944
+ if (target != null && target > 0) {
15945
+ for (const item of data)
15946
+ item.color = this.getThresholdColor(item.value, target);
15947
+ return;
15948
+ }
15949
+ }
15930
15950
  const scheme = this.config?.colorScheme ?? 'green';
15931
15951
  const nonZeroValues = data.filter(d => d.value !== 0).map(d => d.value);
15932
15952
  if (nonZeroValues.length === 0) {
@@ -15941,6 +15961,31 @@ class HeatmapWidgetComponent {
15941
15961
  item.color = this.getColorForValue(item.value, ranges);
15942
15962
  }
15943
15963
  }
15964
+ /**
15965
+ * Resolves the expected value per cell for `threshold` color mode. Uses the
15966
+ * explicit {@link HeatmapWidgetConfig.thresholdTarget} when set; otherwise
15967
+ * auto-derives it from the number of asset-scoped source rtIds (each scoped
15968
+ * source contributes one value per interval, so a "healthy" cell equals the
15969
+ * number of scoped sources — e.g. the EnergyMeasurements under a MeteringPoint).
15970
+ * Returns `null` when neither is available.
15971
+ */
15972
+ resolveThresholdTarget() {
15973
+ const manual = this.config?.thresholdTarget;
15974
+ if (manual != null && manual > 0)
15975
+ return manual;
15976
+ const ds = this.config?.dataSource;
15977
+ const rtIds = this.stateService.resolveStreamDataRtIds(ds?.entitySelectorId);
15978
+ return rtIds && rtIds.length > 0 ? rtIds.length : null;
15979
+ }
15980
+ /**
15981
+ * Bands a value around the target: equal → green, below (incl. empty 0-cells)
15982
+ * → amber, above → red.
15983
+ */
15984
+ getThresholdColor(value, target) {
15985
+ if (value === target)
15986
+ return THRESHOLD_COLORS.ok;
15987
+ return value < target ? THRESHOLD_COLORS.warn : THRESHOLD_COLORS.high;
15988
+ }
15944
15989
  aggregate(values, aggregation) {
15945
15990
  if (values.length === 0)
15946
15991
  return 0;
@@ -16112,6 +16157,8 @@ class HeatmapConfigDialogComponent {
16112
16157
  initialValueField;
16113
16158
  initialAggregation;
16114
16159
  initialColorScheme;
16160
+ initialColorMode;
16161
+ initialThresholdTarget;
16115
16162
  initialShowLegend;
16116
16163
  initialLegendPosition;
16117
16164
  initialDecimalPlaces;
@@ -16137,6 +16184,11 @@ class HeatmapConfigDialogComponent {
16137
16184
  { value: 'sum', label: 'Sum (total of values)' },
16138
16185
  { value: 'avg', label: 'Average (mean of values)' }
16139
16186
  ];
16187
+ // Color mode options
16188
+ colorModeOptions = [
16189
+ { value: 'gradient', label: 'Gradient' },
16190
+ { value: 'threshold', label: 'Threshold' }
16191
+ ];
16140
16192
  // Color scheme options
16141
16193
  colorSchemeOptions = [
16142
16194
  { value: 'green', label: 'Green', previewColor: '#66bb6a' },
@@ -16164,6 +16216,8 @@ class HeatmapConfigDialogComponent {
16164
16216
  valueField: '',
16165
16217
  aggregation: 'count',
16166
16218
  colorScheme: 'green',
16219
+ colorMode: 'gradient',
16220
+ thresholdTarget: undefined,
16167
16221
  showLegend: true,
16168
16222
  legendPosition: 'bottom',
16169
16223
  decimalPlaces: 2,
@@ -16193,6 +16247,8 @@ class HeatmapConfigDialogComponent {
16193
16247
  this.form.valueField = this.initialValueField;
16194
16248
  this.form.aggregation = this.initialAggregation ?? 'count';
16195
16249
  this.form.colorScheme = this.initialColorScheme ?? 'green';
16250
+ this.form.colorMode = this.initialColorMode ?? 'gradient';
16251
+ this.form.thresholdTarget = this.initialThresholdTarget;
16196
16252
  this.form.showLegend = this.initialShowLegend ?? true;
16197
16253
  this.form.legendPosition = this.initialLegendPosition ?? 'bottom';
16198
16254
  this.form.decimalPlaces = this.initialDecimalPlaces ?? 2;
@@ -16323,6 +16379,8 @@ class HeatmapConfigDialogComponent {
16323
16379
  valueField: this.form.valueField || undefined,
16324
16380
  aggregation: this.form.aggregation,
16325
16381
  colorScheme: this.form.colorScheme,
16382
+ colorMode: this.form.colorMode,
16383
+ thresholdTarget: this.form.colorMode === 'threshold' ? (this.form.thresholdTarget ?? undefined) : undefined,
16326
16384
  showLegend: this.form.showLegend,
16327
16385
  legendPosition: this.form.legendPosition,
16328
16386
  decimalPlaces: this.form.decimalPlaces,
@@ -16336,7 +16394,7 @@ class HeatmapConfigDialogComponent {
16336
16394
  this.windowRef.close();
16337
16395
  }
16338
16396
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: HeatmapConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16339
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: HeatmapConfigDialogComponent, isStandalone: true, selector: "mm-heatmap-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialDateField: "initialDateField", initialDateEndField: "initialDateEndField", initialValueField: "initialValueField", initialAggregation: "initialAggregation", initialColorScheme: "initialColorScheme", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialDecimalPlaces: "initialDecimalPlaces", initialCompactNumbers: "initialCompactNumbers", initialValueMultiplier: "initialValueMultiplier", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
16397
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: HeatmapConfigDialogComponent, isStandalone: true, selector: "mm-heatmap-config-dialog", inputs: { initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialDateField: "initialDateField", initialDateEndField: "initialDateEndField", initialValueField: "initialValueField", initialAggregation: "initialAggregation", initialColorScheme: "initialColorScheme", initialColorMode: "initialColorMode", initialThresholdTarget: "initialThresholdTarget", initialShowLegend: "initialShowLegend", initialLegendPosition: "initialLegendPosition", initialDecimalPlaces: "initialDecimalPlaces", initialCompactNumbers: "initialCompactNumbers", initialValueMultiplier: "initialValueMultiplier", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
16340
16398
  <div class="config-container">
16341
16399
 
16342
16400
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -16467,24 +16525,63 @@ class HeatmapConfigDialogComponent {
16467
16525
  <h3 class="section-title">Display Options</h3>
16468
16526
 
16469
16527
  <div class="form-field">
16470
- <label>Color Scheme</label>
16528
+ <label>Color Mode</label>
16471
16529
  <div class="color-scheme-grid">
16472
- @for (scheme of colorSchemeOptions; track scheme.value) {
16530
+ @for (mode of colorModeOptions; track mode.value) {
16473
16531
  <label class="radio-label">
16474
16532
  <input type="radio"
16475
- name="colorScheme"
16476
- [value]="scheme.value"
16477
- [(ngModel)]="form.colorScheme"
16533
+ name="colorMode"
16534
+ [value]="mode.value"
16535
+ [(ngModel)]="form.colorMode"
16478
16536
  kendoRadioButton />
16479
- <span class="color-scheme-preview">
16480
- <span class="color-swatch" [style.background]="scheme.previewColor"></span>
16481
- {{ scheme.label }}
16482
- </span>
16537
+ <span class="color-scheme-preview">{{ mode.label }}</span>
16483
16538
  </label>
16484
16539
  }
16485
16540
  </div>
16541
+ <p class="field-hint">
16542
+ Gradient colors relative to the data's min/max. Threshold colors each cell
16543
+ against an expected count: equal = green, below (incl. empty) = amber, above = red.
16544
+ </p>
16486
16545
  </div>
16487
16546
 
16547
+ @if (form.colorMode === 'gradient') {
16548
+ <div class="form-field">
16549
+ <label>Color Scheme</label>
16550
+ <div class="color-scheme-grid">
16551
+ @for (scheme of colorSchemeOptions; track scheme.value) {
16552
+ <label class="radio-label">
16553
+ <input type="radio"
16554
+ name="colorScheme"
16555
+ [value]="scheme.value"
16556
+ [(ngModel)]="form.colorScheme"
16557
+ kendoRadioButton />
16558
+ <span class="color-scheme-preview">
16559
+ <span class="color-swatch" [style.background]="scheme.previewColor"></span>
16560
+ {{ scheme.label }}
16561
+ </span>
16562
+ </label>
16563
+ }
16564
+ </div>
16565
+ </div>
16566
+ } @else {
16567
+ <div class="form-field">
16568
+ <label>Target Value</label>
16569
+ <kendo-numerictextbox
16570
+ [(ngModel)]="form.thresholdTarget"
16571
+ [min]="0"
16572
+ [step]="1"
16573
+ [decimals]="0"
16574
+ format="n0"
16575
+ placeholder="Auto (number of scoped sources)">
16576
+ </kendo-numerictextbox>
16577
+ <p class="field-hint">
16578
+ Expected value per cell. Leave empty to auto-derive it from the number of
16579
+ asset-scoped sources (e.g. the EnergyMeasurements under the picked MeteringPoint),
16580
+ which requires a "Scope to entity selector" binding above.
16581
+ </p>
16582
+ </div>
16583
+ }
16584
+
16488
16585
  <div class="form-field">
16489
16586
  <label>Decimal Places</label>
16490
16587
  <kendo-numerictextbox
@@ -16703,24 +16800,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16703
16800
  <h3 class="section-title">Display Options</h3>
16704
16801
 
16705
16802
  <div class="form-field">
16706
- <label>Color Scheme</label>
16803
+ <label>Color Mode</label>
16707
16804
  <div class="color-scheme-grid">
16708
- @for (scheme of colorSchemeOptions; track scheme.value) {
16805
+ @for (mode of colorModeOptions; track mode.value) {
16709
16806
  <label class="radio-label">
16710
16807
  <input type="radio"
16711
- name="colorScheme"
16712
- [value]="scheme.value"
16713
- [(ngModel)]="form.colorScheme"
16808
+ name="colorMode"
16809
+ [value]="mode.value"
16810
+ [(ngModel)]="form.colorMode"
16714
16811
  kendoRadioButton />
16715
- <span class="color-scheme-preview">
16716
- <span class="color-swatch" [style.background]="scheme.previewColor"></span>
16717
- {{ scheme.label }}
16718
- </span>
16812
+ <span class="color-scheme-preview">{{ mode.label }}</span>
16719
16813
  </label>
16720
16814
  }
16721
16815
  </div>
16816
+ <p class="field-hint">
16817
+ Gradient colors relative to the data's min/max. Threshold colors each cell
16818
+ against an expected count: equal = green, below (incl. empty) = amber, above = red.
16819
+ </p>
16722
16820
  </div>
16723
16821
 
16822
+ @if (form.colorMode === 'gradient') {
16823
+ <div class="form-field">
16824
+ <label>Color Scheme</label>
16825
+ <div class="color-scheme-grid">
16826
+ @for (scheme of colorSchemeOptions; track scheme.value) {
16827
+ <label class="radio-label">
16828
+ <input type="radio"
16829
+ name="colorScheme"
16830
+ [value]="scheme.value"
16831
+ [(ngModel)]="form.colorScheme"
16832
+ kendoRadioButton />
16833
+ <span class="color-scheme-preview">
16834
+ <span class="color-swatch" [style.background]="scheme.previewColor"></span>
16835
+ {{ scheme.label }}
16836
+ </span>
16837
+ </label>
16838
+ }
16839
+ </div>
16840
+ </div>
16841
+ } @else {
16842
+ <div class="form-field">
16843
+ <label>Target Value</label>
16844
+ <kendo-numerictextbox
16845
+ [(ngModel)]="form.thresholdTarget"
16846
+ [min]="0"
16847
+ [step]="1"
16848
+ [decimals]="0"
16849
+ format="n0"
16850
+ placeholder="Auto (number of scoped sources)">
16851
+ </kendo-numerictextbox>
16852
+ <p class="field-hint">
16853
+ Expected value per cell. Leave empty to auto-derive it from the number of
16854
+ asset-scoped sources (e.g. the EnergyMeasurements under the picked MeteringPoint),
16855
+ which requires a "Scope to entity selector" binding above.
16856
+ </p>
16857
+ </div>
16858
+ }
16859
+
16724
16860
  <div class="form-field">
16725
16861
  <label>Decimal Places</label>
16726
16862
  <kendo-numerictextbox
@@ -16816,6 +16952,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
16816
16952
  type: Input
16817
16953
  }], initialColorScheme: [{
16818
16954
  type: Input
16955
+ }], initialColorMode: [{
16956
+ type: Input
16957
+ }], initialThresholdTarget: [{
16958
+ type: Input
16819
16959
  }], initialShowLegend: [{
16820
16960
  type: Input
16821
16961
  }], initialLegendPosition: [{
@@ -29051,6 +29191,8 @@ function registerDefaultWidgets(registry) {
29051
29191
  initialValueField: heatmapWidget.valueField,
29052
29192
  initialAggregation: heatmapWidget.aggregation,
29053
29193
  initialColorScheme: heatmapWidget.colorScheme,
29194
+ initialColorMode: heatmapWidget.colorMode,
29195
+ initialThresholdTarget: heatmapWidget.thresholdTarget,
29054
29196
  initialShowLegend: heatmapWidget.showLegend,
29055
29197
  initialLegendPosition: heatmapWidget.legendPosition,
29056
29198
  initialDecimalPlaces: heatmapWidget.decimalPlaces,
@@ -29081,6 +29223,8 @@ function registerDefaultWidgets(registry) {
29081
29223
  valueField: result.valueField,
29082
29224
  aggregation: result.aggregation,
29083
29225
  colorScheme: result.colorScheme,
29226
+ colorMode: result.colorMode,
29227
+ thresholdTarget: result.thresholdTarget,
29084
29228
  showLegend: result.showLegend,
29085
29229
  legendPosition: result.legendPosition,
29086
29230
  decimalPlaces: result.decimalPlaces,
@@ -29112,6 +29256,8 @@ function registerDefaultWidgets(registry) {
29112
29256
  valueField: widget.valueField,
29113
29257
  aggregation: widget.aggregation,
29114
29258
  colorScheme: widget.colorScheme,
29259
+ ...(widget.colorMode && { colorMode: widget.colorMode }),
29260
+ ...(widget.thresholdTarget != null && { thresholdTarget: widget.thresholdTarget }),
29115
29261
  showLegend: widget.showLegend,
29116
29262
  legendPosition: widget.legendPosition,
29117
29263
  decimalPlaces: widget.decimalPlaces,
@@ -29145,6 +29291,8 @@ function registerDefaultWidgets(registry) {
29145
29291
  valueField: config['valueField'],
29146
29292
  aggregation: config['aggregation'] ?? 'count',
29147
29293
  colorScheme: config['colorScheme'] ?? 'green',
29294
+ colorMode: config['colorMode'],
29295
+ thresholdTarget: config['thresholdTarget'],
29148
29296
  showLegend: config['showLegend'] ?? true,
29149
29297
  legendPosition: config['legendPosition'] ?? 'bottom',
29150
29298
  decimalPlaces: config['decimalPlaces'],