@record-evolution/widget-gauge 1.7.35 → 1.7.36

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
@@ -258,20 +263,23 @@ class WidgetGauge extends LitElement {
258
263
  // )
259
264
  this.boxes = Array.from(this.gaugeContainer?.querySelectorAll('.chart') as NodeListOf<HTMLDivElement>)
260
265
 
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
- )
266
+ const gridColumns = `repeat(${fit.c}, 1fr)`
267
+ if (this.gaugeContainer.style.gridTemplateColumns !== gridColumns)
268
+ this.gaugeContainer.style.gridTemplateColumns = gridColumns
269
+
270
+ // This runs on every data tick, not just on real resizes. Touching the
271
+ // style attribute and calling echart.resize() when nothing changed is
272
+ // not free: each resize is a full unanimated re-render that also snaps
273
+ // any running gauge animation, so six gauges hitched on every update.
274
+ // Only boxes whose target size actually differs are written and resized.
275
+ const boxStyle = `width:${modifier * chartW}px; height:${modifier * (chartH - this.textContainerHeight)}px`
276
+ this.boxes?.forEach((box) => {
277
+ if (box.getAttribute('style') === boxStyle) return
278
+ box.setAttribute('style', boxStyle)
279
+ echarts.getInstanceByDom(box)?.resize()
280
+ })
269
281
 
270
282
  this.modifier = modifier
271
-
272
- this.canvasList.forEach((canvasObj) => {
273
- canvasObj.echart?.resize()
274
- })
275
283
  }
276
284
 
277
285
  async transformData() {
@@ -336,45 +344,16 @@ class WidgetGauge extends LitElement {
336
344
  }
337
345
  ds.needleValue = isNaN(ds.needleValue as number) ? gaugeMin : ds.needleValue
338
346
 
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)) {
347
+ // Blank the gauge when the data is stale. The newest row sits at the
348
+ // END of the data array (the needle averages `slice(-averageLatest)`),
349
+ // so that is the row whose age matters. Rows without a parseable tsp
350
+ // opt out of the check without timestamps age cannot be judged.
351
+ const tsp = Date.parse(ds.data?.[ds.data.length - 1]?.tsp ?? '')
352
+ if (!isNaN(tsp)) {
358
353
  const now = new Date().getTime()
359
354
  if (now - tsp > (ds.advanced?.maxLatency ?? Infinity) * 1000) ds.needleValue = undefined
360
355
  }
361
356
 
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
357
  // Axis
379
358
  const defaultColors = ['#bf444c', '#d88273', '#f6efa6']
380
359
  const themeColors = this.theme?.theme_object?.color ?? defaultColors
@@ -409,9 +388,6 @@ class WidgetGauge extends LitElement {
409
388
  const gaugeMax = sectionLimits?.[sectionLimits.length - 1] ?? 100
410
389
  ds.range = Math.abs(gaugeMax - gaugeMin)
411
390
 
412
- ga.min = ga2.min = gaugeMin
413
- ga.max = ga2.max = gaugeMax
414
-
415
391
  // percentages of the sections paired with the colors (must be strictly ascending for ECharts)
416
392
  const colorSections = sectionLimits
417
393
  .map((limit, i) => {
@@ -421,14 +397,6 @@ class WidgetGauge extends LitElement {
421
397
  })
422
398
  .filter(([s]) => !isNaN(s) && s >= 0)
423
399
 
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
400
  // Progress
433
401
  let progressColor = colors?.[colors.length - 1]
434
402
  for (const [i, s] of sectionLimits?.entries() ?? []) {
@@ -440,10 +408,89 @@ class WidgetGauge extends LitElement {
440
408
  break
441
409
  }
442
410
  }
411
+
412
+ const canvas = this.canvasList.get(ds.label)
413
+ const echart = canvas?.echart
414
+
415
+ // On a live dashboard this loop runs for every gauge on every data
416
+ // tick, so the steady-state path has to stay off the expensive
417
+ // full-option merge: everything below except the needle value and
418
+ // the progress color is configuration that only changes with the
419
+ // container size (modifier), the widget config, or the theme. When
420
+ // that signature is unchanged, send ECharts only the two values
421
+ // that move; the stored option keeps everything else. lazyUpdate
422
+ // defers processing to the next animation frame so several gauges
423
+ // updating in the same tick do not each force a synchronous render.
424
+ const configSig = JSON.stringify([
425
+ modifier,
426
+ ds.unit,
427
+ ds.precision,
428
+ ds.valueColor,
429
+ ds.sections,
430
+ this.themeTitleColor
431
+ ])
432
+ if (canvas && canvas.lastConfig === configSig) {
433
+ echart?.setOption(
434
+ {
435
+ series: [
436
+ {
437
+ data: [{ value: ds.needleValue ?? null, name: ds.unit }],
438
+ progress: { itemStyle: { color: progressColor } }
439
+ }
440
+ ]
441
+ },
442
+ { lazyUpdate: true }
443
+ )
444
+ continue
445
+ }
446
+ if (canvas) canvas.lastConfig = configSig
447
+
448
+ // Always build the option from the template — never from getOption().
449
+ // Reading the rendered option back and handing it to setOption looks
450
+ // cheap but is not: getOption() deep-clones the whole stored option on
451
+ // every frame, and it normalizes every component to an array, so any
452
+ // future `{ ...option.x }` here would spread an array into `{ '0': x }`
453
+ // and ECharts would merge that back one level deeper each update until
454
+ // zrender's recursive merge() overflowed the stack. Both gauge series
455
+ // and every nested key touched below are declared in the template, so
456
+ // a clone of it is a complete option in its own right.
457
+ const option = window.structuredClone(this.template)
458
+ const seriesArr = option.series as GaugeSeriesOption[]
459
+ const ga: any = seriesArr?.[0],
460
+ ga2: any = seriesArr?.[1]
461
+
462
+ ga.data[0].value = ds.needleValue
463
+ ga.data[0].name = ds.unit
464
+ // unit style
465
+
466
+ ga.title.fontSize = 32 * modifier
467
+ ga.title.color = ds.valueColor || this.themeTitleColor
468
+ ga.title.opacity = 1
469
+ // value style
470
+ ga.detail.color = ds.valueColor || this.themeTitleColor
471
+ ga.detail.opacity = 1
472
+ ga.detail.fontSize = 60 * modifier
473
+
474
+ ga.detail.formatter = (val: number) => {
475
+ const num = Number(val)
476
+ return isNaN(num) ? '-' : num.toFixed(Math.floor(ds.precision ?? 0))
477
+ }
478
+
479
+ ga.min = ga2.min = gaugeMin
480
+ ga.max = ga2.max = gaugeMax
481
+
482
+ ga2.axisLine.lineStyle.width = 8 * modifier
483
+ if (colorSections.length) ga2.axisLine.lineStyle.color = colorSections
484
+ ga2.axisLabel.fontSize = 24 * modifier
485
+ // ga2.axisLabel.color = ds.valueColor
486
+ ga2.axisLabel.distance = -24 * modifier
487
+ ga2.splitLine.length = 16 * modifier
488
+ ga2.splitLine.distance = -16 * modifier
489
+
443
490
  ga.progress.itemStyle.color = progressColor
444
491
  ga.progress.width = 60 * modifier
445
492
 
446
- const titleElement = this.canvasList.get(ds.label)?.title
493
+ const titleElement = canvas?.title
447
494
  if (titleElement) {
448
495
  titleElement.style.fontSize = String(36 * modifier) + 'px'
449
496
  titleElement.style.maxWidth = String(550 * modifier) + 'px'
@@ -452,7 +499,7 @@ class WidgetGauge extends LitElement {
452
499
  }
453
500
 
454
501
  // Apply
455
- echart?.setOption(option)
502
+ echart?.setOption(option, { lazyUpdate: true })
456
503
  }
457
504
  }
458
505
  deleteCharts() {