@record-evolution/widget-gauge 1.7.35 → 1.7.37

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.
@@ -32,7 +32,12 @@ class WidgetGauge extends LitElement {
32
32
  @state()
33
33
  private canvasList: Map<
34
34
  string,
35
- { echart?: echarts.ECharts; title?: HTMLHeadingElement; wrapper?: HTMLDivElement }
35
+ {
36
+ echart?: echarts.ECharts
37
+ title?: HTMLHeadingElement
38
+ wrapper?: HTMLDivElement
39
+ lastConfig?: string
40
+ }
36
41
  > = new Map()
37
42
 
38
43
  @state() private themeBgColor?: string
@@ -60,6 +65,16 @@ class WidgetGauge extends LitElement {
60
65
  })
61
66
 
62
67
  this.template = {
68
+ // The registered ECharts theme carries its own canvas
69
+ // `backgroundColor` (chalk paints `rgba(41,52,65,1)`), which the
70
+ // gauge canvas draws as an opaque slab on top of the wrapper's
71
+ // background. That hides whatever colour the tile actually has —
72
+ // the `--re-tile-background-color` the host injects, or a user's
73
+ // custom colour — so the gauge sits in a differently coloured box.
74
+ // Forcing the option's background to transparent lets the wrapper's
75
+ // background show through; the wrapper already falls back to the
76
+ // theme's own colour when no override is set, so nothing is lost.
77
+ backgroundColor: 'transparent',
63
78
  series: [
64
79
  {
65
80
  type: 'gauge',
@@ -258,20 +273,23 @@ class WidgetGauge extends LitElement {
258
273
  // )
259
274
  this.boxes = Array.from(this.gaugeContainer?.querySelectorAll('.chart') as NodeListOf<HTMLDivElement>)
260
275
 
261
- this.gaugeContainer.style.gridTemplateColumns = `repeat(${fit.c}, 1fr)`
262
-
263
- this.boxes?.forEach((box) =>
264
- box.setAttribute(
265
- 'style',
266
- `width:${modifier * chartW}px; height:${modifier * (chartH - this.textContainerHeight)}px`
267
- )
268
- )
276
+ const gridColumns = `repeat(${fit.c}, 1fr)`
277
+ if (this.gaugeContainer.style.gridTemplateColumns !== gridColumns)
278
+ this.gaugeContainer.style.gridTemplateColumns = gridColumns
279
+
280
+ // This runs on every data tick, not just on real resizes. Touching the
281
+ // style attribute and calling echart.resize() when nothing changed is
282
+ // not free: each resize is a full unanimated re-render that also snaps
283
+ // any running gauge animation, so six gauges hitched on every update.
284
+ // Only boxes whose target size actually differs are written and resized.
285
+ const boxStyle = `width:${modifier * chartW}px; height:${modifier * (chartH - this.textContainerHeight)}px`
286
+ this.boxes?.forEach((box) => {
287
+ if (box.getAttribute('style') === boxStyle) return
288
+ box.setAttribute('style', boxStyle)
289
+ echarts.getInstanceByDom(box)?.resize()
290
+ })
269
291
 
270
292
  this.modifier = modifier
271
-
272
- this.canvasList.forEach((canvasObj) => {
273
- canvasObj.echart?.resize()
274
- })
275
293
  }
276
294
 
277
295
  async transformData() {
@@ -336,45 +354,16 @@ class WidgetGauge extends LitElement {
336
354
  }
337
355
  ds.needleValue = isNaN(ds.needleValue as number) ? gaugeMin : ds.needleValue
338
356
 
339
- const echart = this.canvasList.get(ds.label)?.echart
340
- // Always build the option from the template never from getOption().
341
- // Reading the rendered option back and handing it to setOption looks
342
- // cheap but is not: getOption() deep-clones the whole stored option on
343
- // every frame, and it normalizes every component to an array, so any
344
- // future `{ ...option.x }` here would spread an array into `{ '0': x }`
345
- // and ECharts would merge that back one level deeper each update until
346
- // zrender's recursive merge() overflowed the stack. Both gauge series
347
- // and every nested key touched below are declared in the template, so
348
- // a clone of it is a complete option in its own right.
349
- const option = window.structuredClone(this.template)
350
- const seriesArr = option.series as GaugeSeriesOption[]
351
- const ga: any = seriesArr?.[0],
352
- ga2: any = seriesArr?.[1]
353
-
354
- // Needle
355
- // Check age of data Latency
356
- const tsp = Date.parse(ds?.data?.[0]?.tsp ?? '')
357
- if (isNaN(tsp)) {
357
+ // Blank the gauge when the data is stale. The newest row sits at the
358
+ // END of the data array (the needle averages `slice(-averageLatest)`),
359
+ // so that is the row whose age matters. Rows without a parseable tsp
360
+ // opt out of the check without timestamps age cannot be judged.
361
+ const tsp = Date.parse(ds.data?.[ds.data.length - 1]?.tsp ?? '')
362
+ if (!isNaN(tsp)) {
358
363
  const now = new Date().getTime()
359
364
  if (now - tsp > (ds.advanced?.maxLatency ?? Infinity) * 1000) ds.needleValue = undefined
360
365
  }
361
366
 
362
- ga.data[0].value = ds.needleValue
363
- ga.data[0].name = ds.unit
364
- // unit style
365
-
366
- ga.title.fontSize = 32 * modifier
367
- ga.title.color = ds.valueColor || this.themeTitleColor
368
- ga.title.opacity = 1
369
- // value style
370
- ga.detail.color = ds.valueColor || this.themeTitleColor
371
- ga.detail.opacity = 1
372
- ga.detail.fontSize = 60 * modifier
373
-
374
- ga.detail.formatter = (val: number) => {
375
- const num = Number(val)
376
- return isNaN(num) ? '-' : num.toFixed(Math.floor(ds.precision ?? 0))
377
- }
378
367
  // Axis
379
368
  const defaultColors = ['#bf444c', '#d88273', '#f6efa6']
380
369
  const themeColors = this.theme?.theme_object?.color ?? defaultColors
@@ -409,9 +398,6 @@ class WidgetGauge extends LitElement {
409
398
  const gaugeMax = sectionLimits?.[sectionLimits.length - 1] ?? 100
410
399
  ds.range = Math.abs(gaugeMax - gaugeMin)
411
400
 
412
- ga.min = ga2.min = gaugeMin
413
- ga.max = ga2.max = gaugeMax
414
-
415
401
  // percentages of the sections paired with the colors (must be strictly ascending for ECharts)
416
402
  const colorSections = sectionLimits
417
403
  .map((limit, i) => {
@@ -421,14 +407,6 @@ class WidgetGauge extends LitElement {
421
407
  })
422
408
  .filter(([s]) => !isNaN(s) && s >= 0)
423
409
 
424
- ga2.axisLine.lineStyle.width = 8 * modifier
425
- if (colorSections.length) ga2.axisLine.lineStyle.color = colorSections
426
- ga2.axisLabel.fontSize = 24 * modifier
427
- // ga2.axisLabel.color = ds.valueColor
428
- ga2.axisLabel.distance = -24 * modifier
429
- ga2.splitLine.length = 16 * modifier
430
- ga2.splitLine.distance = -16 * modifier
431
-
432
410
  // Progress
433
411
  let progressColor = colors?.[colors.length - 1]
434
412
  for (const [i, s] of sectionLimits?.entries() ?? []) {
@@ -440,10 +418,89 @@ class WidgetGauge extends LitElement {
440
418
  break
441
419
  }
442
420
  }
421
+
422
+ const canvas = this.canvasList.get(ds.label)
423
+ const echart = canvas?.echart
424
+
425
+ // On a live dashboard this loop runs for every gauge on every data
426
+ // tick, so the steady-state path has to stay off the expensive
427
+ // full-option merge: everything below except the needle value and
428
+ // the progress color is configuration that only changes with the
429
+ // container size (modifier), the widget config, or the theme. When
430
+ // that signature is unchanged, send ECharts only the two values
431
+ // that move; the stored option keeps everything else. lazyUpdate
432
+ // defers processing to the next animation frame so several gauges
433
+ // updating in the same tick do not each force a synchronous render.
434
+ const configSig = JSON.stringify([
435
+ modifier,
436
+ ds.unit,
437
+ ds.precision,
438
+ ds.valueColor,
439
+ ds.sections,
440
+ this.themeTitleColor
441
+ ])
442
+ if (canvas && canvas.lastConfig === configSig) {
443
+ echart?.setOption(
444
+ {
445
+ series: [
446
+ {
447
+ data: [{ value: ds.needleValue ?? null, name: ds.unit }],
448
+ progress: { itemStyle: { color: progressColor } }
449
+ }
450
+ ]
451
+ },
452
+ { lazyUpdate: true }
453
+ )
454
+ continue
455
+ }
456
+ if (canvas) canvas.lastConfig = configSig
457
+
458
+ // Always build the option from the template — never from getOption().
459
+ // Reading the rendered option back and handing it to setOption looks
460
+ // cheap but is not: getOption() deep-clones the whole stored option on
461
+ // every frame, and it normalizes every component to an array, so any
462
+ // future `{ ...option.x }` here would spread an array into `{ '0': x }`
463
+ // and ECharts would merge that back one level deeper each update until
464
+ // zrender's recursive merge() overflowed the stack. Both gauge series
465
+ // and every nested key touched below are declared in the template, so
466
+ // a clone of it is a complete option in its own right.
467
+ const option = window.structuredClone(this.template)
468
+ const seriesArr = option.series as GaugeSeriesOption[]
469
+ const ga: any = seriesArr?.[0],
470
+ ga2: any = seriesArr?.[1]
471
+
472
+ ga.data[0].value = ds.needleValue
473
+ ga.data[0].name = ds.unit
474
+ // unit style
475
+
476
+ ga.title.fontSize = 32 * modifier
477
+ ga.title.color = ds.valueColor || this.themeTitleColor
478
+ ga.title.opacity = 1
479
+ // value style
480
+ ga.detail.color = ds.valueColor || this.themeTitleColor
481
+ ga.detail.opacity = 1
482
+ ga.detail.fontSize = 60 * modifier
483
+
484
+ ga.detail.formatter = (val: number) => {
485
+ const num = Number(val)
486
+ return isNaN(num) ? '-' : num.toFixed(Math.floor(ds.precision ?? 0))
487
+ }
488
+
489
+ ga.min = ga2.min = gaugeMin
490
+ ga.max = ga2.max = gaugeMax
491
+
492
+ ga2.axisLine.lineStyle.width = 8 * modifier
493
+ if (colorSections.length) ga2.axisLine.lineStyle.color = colorSections
494
+ ga2.axisLabel.fontSize = 24 * modifier
495
+ // ga2.axisLabel.color = ds.valueColor
496
+ ga2.axisLabel.distance = -24 * modifier
497
+ ga2.splitLine.length = 16 * modifier
498
+ ga2.splitLine.distance = -16 * modifier
499
+
443
500
  ga.progress.itemStyle.color = progressColor
444
501
  ga.progress.width = 60 * modifier
445
502
 
446
- const titleElement = this.canvasList.get(ds.label)?.title
503
+ const titleElement = canvas?.title
447
504
  if (titleElement) {
448
505
  titleElement.style.fontSize = String(36 * modifier) + 'px'
449
506
  titleElement.style.maxWidth = String(550 * modifier) + 'px'
@@ -452,7 +509,7 @@ class WidgetGauge extends LitElement {
452
509
  }
453
510
 
454
511
  // Apply
455
- echart?.setOption(option)
512
+ echart?.setOption(option, { lazyUpdate: true })
456
513
  }
457
514
  }
458
515
  deleteCharts() {