iobroker.sun2000 2.4.4 → 2.5.0

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.
package/lib/register.js CHANGED
@@ -4,7 +4,7 @@ const { deviceType, driverClasses, dataRefreshRate } = require(`${__dirname}/typ
4
4
  const { RiemannSum, StateMap } = require(`${__dirname}/tools.js`);
5
5
  const getDriverHandler = require(`${__dirname}/drivers/index.js`);
6
6
  const tools = require(`${__dirname}/tools.js`);
7
- const statistics = require(`${__dirname}/statistics.js`);
7
+ const statistics = require(`${__dirname}/statistics/statistics.js`);
8
8
 
9
9
  class Registers {
10
10
  constructor(adapterInstance) {
@@ -12,6 +12,14 @@ class Registers {
12
12
  this.stateCache = new StateMap();
13
13
 
14
14
  this.externalSum = new RiemannSum();
15
+ this.inPowerSum = new RiemannSum();
16
+ this.actPowerSum = new RiemannSum();
17
+ this.chargeSum = new RiemannSum();
18
+ this.dischargeSum = new RiemannSum();
19
+ this.houseConsumSum = new RiemannSum();
20
+ this.feedinSum = new RiemannSum();
21
+ this.feedoutSum = new RiemannSum();
22
+
15
23
  this.statistics = new statistics(adapterInstance, this.stateCache);
16
24
 
17
25
  for (const device of this.adapter.devices) {
@@ -34,9 +42,84 @@ class Registers {
34
42
  }
35
43
  })
36
44
  );
45
+ //Upgrade to v2.5.0 - deleted deprecated states
46
+ /*
47
+ if (
48
+ tools.existsState(this.adapter, `collected.consumptionStart`, (err, exists) => {
49
+ if (!err && exists) {
50
+ tools.deleteState(this.adapter, `collected.consumptionStart`, (err, deleted) => {
51
+ if (!err && deleted) {
52
+ this.adapter.logger.debug('Deleted deprecated state collected.consumptionStart');
53
+ }
54
+ });
55
+ }
56
+ })
57
+ );
58
+ */
37
59
 
38
60
  //this.postProcessHooks = [];
39
61
  this.postProcessHooks = [
62
+ {
63
+ refresh: dataRefreshRate.high,
64
+ states: [
65
+ {
66
+ id: 'collected.sumDaily.houseConsumption',
67
+ name: 'daily sum of House consumption',
68
+ type: 'number',
69
+ unit: 'kWh',
70
+ role: 'value.power.consumed',
71
+ desc: 'riemann sum of Load power',
72
+ },
73
+ {
74
+ id: 'collected.sumDaily.activePower',
75
+ name: 'daily sum of Active power',
76
+ type: 'number',
77
+ unit: 'kWh',
78
+ role: 'value.power.produced',
79
+ desc: 'riemann sum of Power currently used',
80
+ },
81
+ {
82
+ id: 'collected.sumDaily.inputPower',
83
+ name: 'daily sum of Input power',
84
+ type: 'number',
85
+ unit: 'kWh',
86
+ role: 'value.power.produced',
87
+ desc: 'riemann sum of Power from solar',
88
+ },
89
+ {
90
+ id: 'collected.sumDaily.chargePower',
91
+ name: 'daily sum of Charge power',
92
+ type: 'number',
93
+ unit: 'kWh',
94
+ role: 'value.power.consumed',
95
+ desc: 'riemann sum of Charge power',
96
+ },
97
+ {
98
+ id: 'collected.sumDaily.dischargePower',
99
+ name: 'daily sum of Discharge power',
100
+ type: 'number',
101
+ unit: 'kWh',
102
+ role: 'value.power.produced',
103
+ desc: 'riemann sum of Discharge power',
104
+ },
105
+ {
106
+ id: 'collected.sumDaily.feed-inPower',
107
+ name: 'daily sum of Feed-in power',
108
+ type: 'number',
109
+ unit: 'kWh',
110
+ role: 'value.power.consumed',
111
+ desc: 'riemann sum of Feed-in power',
112
+ },
113
+ {
114
+ id: 'collected.sumDaily.feed-outPower',
115
+ name: 'daily sum of Feed-out power',
116
+ type: 'number',
117
+ unit: 'kWh',
118
+ role: 'value.power.produced',
119
+ desc: 'riemann sum of Feed-out power',
120
+ },
121
+ ],
122
+ },
40
123
  {
41
124
  refresh: dataRefreshRate.high,
42
125
  states: [
@@ -175,6 +258,23 @@ class Registers {
175
258
  if (houseConsum < 0) {
176
259
  houseConsum = 0;
177
260
  }
261
+
262
+ //dailySum
263
+ this.inPowerSum.add(inPower);
264
+ this.actPowerSum.add(actPower);
265
+ this.chargeSum.add(chargeDischarge > 0 ? chargeDischarge : 0);
266
+ this.dischargeSum.add(chargeDischarge < 0 ? -chargeDischarge : 0);
267
+ this.houseConsumSum.add(houseConsum);
268
+ this.feedinSum.add(feedinPower > 0 ? feedinPower : 0);
269
+ this.feedoutSum.add(feedinPower < 0 ? -feedinPower : 0);
270
+ this.stateCache.set('collected.sumDaily.houseConsumption', this.houseConsumSum.sum, { type: 'number' });
271
+ this.stateCache.set('collected.sumDaily.activePower', this.actPowerSum.sum, { type: 'number' });
272
+ this.stateCache.set('collected.sumDaily.inputPower', this.inPowerSum.sum, { type: 'number' });
273
+ this.stateCache.set('collected.sumDaily.chargePower', this.chargeSum.sum, { type: 'number' });
274
+ this.stateCache.set('collected.sumDaily.dischargePower', this.dischargeSum.sum, { type: 'number' });
275
+ this.stateCache.set('collected.sumDaily.feed-inPower', this.feedinSum.sum, { type: 'number' });
276
+ this.stateCache.set('collected.sumDaily.feed-outPower', this.feedoutSum.sum, { type: 'number' });
277
+
178
278
  //Überschuss (Differenz)
179
279
  const surplusArray = calcUsableSurplus.bind(this)();
180
280
 
@@ -203,7 +303,7 @@ class Registers {
203
303
  name: 'Daily energy yield',
204
304
  type: 'number',
205
305
  unit: 'kWh',
206
- role: 'value.power.consumption',
306
+ role: 'value.power.produced',
207
307
  desc: 'daily energy yield of the inverters',
208
308
  },
209
309
  {
@@ -211,7 +311,7 @@ class Registers {
211
311
  name: 'Daily input yield',
212
312
  type: 'number',
213
313
  unit: 'kWh',
214
- role: 'value.power.consumption',
314
+ role: 'value.power.produced',
215
315
  desc: 'yield from the portal',
216
316
  },
217
317
  {
@@ -219,32 +319,38 @@ class Registers {
219
319
  name: 'Daily solar yield',
220
320
  type: 'number',
221
321
  unit: 'kWh',
222
- role: 'value.power.consumption',
322
+ role: 'value.power.produced',
223
323
  desc: 'Riemann sum of input power with efficiency loss',
224
324
  },
225
- { id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
226
- { id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
227
- { id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
228
- { id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
229
- { id: 'collected.consumptionStart', name: 'Consumption start today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
230
- { id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
231
- { id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
232
- { id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
233
- { id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
234
- { id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.consumption' },
325
+ { id: 'collected.accumulatedEnergyYield', name: 'Accumulated energy yield', type: 'number', unit: 'kWh', role: 'value.power.produced' },
326
+ { id: 'collected.consumptionSum', name: 'Consumption sum', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
327
+ { id: 'collected.gridExportStart', name: 'Grid export start today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
328
+ { id: 'collected.gridImportStart', name: 'Grid import start today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
329
+ {
330
+ id: 'collected.consumptionStart',
331
+ name: 'deprecated - Consumption start today',
332
+ type: 'number',
333
+ unit: 'kWh',
334
+ role: 'value.power.consumed',
335
+ },
336
+ { id: 'collected.gridExportToday', name: 'Grid export today', type: 'number', unit: 'kWh', role: 'value.power.produced' },
337
+ { id: 'collected.gridImportToday', name: 'Grid import today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
338
+ { id: 'collected.consumptionToday', name: 'Consumption today', type: 'number', unit: 'kWh', role: 'value.power.consumed' },
339
+ { id: 'collected.totalCharge', name: 'Total charge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
340
+ { id: 'collected.totalDischarge', name: 'Total discharge of battery', type: 'number', unit: 'kWh', role: 'value.power.produced' },
235
341
  {
236
342
  id: 'collected.currentDayChargeCapacity',
237
343
  name: 'Current day charge capacity of battery',
238
344
  type: 'number',
239
345
  unit: 'kWh',
240
- role: 'value.power.consumption',
346
+ role: 'value.power.consumed',
241
347
  },
242
348
  {
243
349
  id: 'collected.currentDayDischargeCapacity',
244
350
  name: 'Current day discharge capacity of battery',
245
351
  type: 'number',
246
352
  unit: 'kWh',
247
- role: 'value.power.consumption',
353
+ role: 'value.power.produced',
248
354
  desc: '',
249
355
  },
250
356
  { id: 'collected.SOC', name: 'State of battery capacity', type: 'number', unit: '%', role: 'value.battery', desc: 'SOC' },
@@ -254,7 +360,7 @@ class Registers {
254
360
  name: 'daily external yield',
255
361
  type: 'number',
256
362
  unit: 'kWh',
257
- role: 'value.power.consumption',
363
+ role: 'value.power.produced',
258
364
  desc: 'Riemann sum of external power',
259
365
  },
260
366
  /*
@@ -263,7 +369,7 @@ class Registers {
263
369
  name: 'Active Energy today',
264
370
  type: 'number',
265
371
  unit: 'kWh',
266
- role: 'value.power.consumption',
372
+ role: 'value.power.produced',
267
373
  desc: 'Amount of Riemann sum of sum of active power',
268
374
  },
269
375
  */
@@ -280,17 +386,20 @@ class Registers {
280
386
  let totalCharge = 0;
281
387
  let ratedCap = 0;
282
388
  let load = 0;
283
- let feedinEnergy;
284
- let supplyFromGrid;
389
+ let ready = false;
285
390
 
286
391
  for (const inverter of inverters) {
287
392
  if (inverter.driverClass != driverClasses.inverter) {
288
393
  continue;
289
394
  }
395
+ if (this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value === undefined) {
396
+ continue; //wait until value is available - otherwise we would overwrite the value from the portal with 0
397
+ }
398
+ ready = true;
290
399
  outYield += this.stateCache.get(`${inverter.path}.dailyEnergyYield`)?.value ?? 0;
291
- inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value;
292
- solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value;
293
- enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value;
400
+ inYield += this.stateCache.get(`${inverter.path}.derived.dailyInputYield`)?.value ?? 0;
401
+ solarYield += this.stateCache.get(`${inverter.path}.derived.dailySolarYield`)?.value ?? 0;
402
+ enYield += this.stateCache.get(`${inverter.path}.accumulatedEnergyYield`)?.value ?? 0;
294
403
  activeEnergy += this.stateCache.get(`${inverter.path}.derived.dailyActiveEnergy`)?.value ?? 0;
295
404
  if (this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value > 0) {
296
405
  charge += this.stateCache.get(`${inverter.path}.battery.currentDayChargeCapacity`)?.value ?? 0;
@@ -300,9 +409,12 @@ class Registers {
300
409
  load +=
301
410
  this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value *
302
411
  this.stateCache.get(`${inverter.path}.battery.SOC`)?.value;
303
- ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value;
412
+ ratedCap += this.stateCache.get(`${inverter.path}.battery.ratedCapacity`)?.value ?? 0;
304
413
  }
305
414
  }
415
+ if (!ready) {
416
+ return; //no inverter ready yet - do not set values
417
+ }
306
418
  //this.stateCache.set('collected.dailyActiveEnergy', activeEnergy, { type: 'number' });
307
419
  this.stateCache.set('collected.dailyExternalYield', this.externalSum.sum, { type: 'number' }); //of externalPower
308
420
  this.stateCache.set('collected.dailyEnergyYield', outYield, { type: 'number' });
@@ -310,9 +422,10 @@ class Registers {
310
422
  this.stateCache.set('collected.dailySolarYield', solarYield, { type: 'number' });
311
423
  this.stateCache.set('collected.accumulatedEnergyYield', enYield, { type: 'number' });
312
424
  const sign = this.stateCache.get('meter.derived.signConventionForPowerFeed-in')?.value ?? 1;
425
+ let feedinEnergy;
426
+ let supplyFromGrid;
313
427
  if (sign === -1) {
314
428
  //Emma Meter Power is positive when power is taken from grid
315
- //Emma Meter Power is negative when power is feed in to grid
316
429
  feedinEnergy = this.stateCache.get('meter.reverseActiveEnergy')?.value ?? 0;
317
430
  supplyFromGrid = this.stateCache.get('meter.positiveActiveEnergy')?.value ?? 0;
318
431
  } else {
@@ -354,7 +467,7 @@ class Registers {
354
467
  this.stateCache.set('collected.currentDayChargeCapacity', charge, { type: 'number' });
355
468
  this.stateCache.set('collected.currentDayDischargeCapacity', disCharge, { type: 'number' });
356
469
  this.stateCache.set('collected.ratedCapacity', ratedCap, { type: 'number' });
357
- this.stateCache.set('collected.SOC', Math.round(load / ratedCap), { type: 'number' });
470
+ this.stateCache.set('collected.SOC', ratedCap ? Math.round(load / ratedCap) : 0, { type: 'number' });
358
471
  },
359
472
  },
360
473
  ];
@@ -475,11 +588,34 @@ class Registers {
475
588
  this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
476
589
  state = await this.adapter.getState('collected.gridImportStart');
477
590
  this.stateCache.set('collected.gridImportStart', state?.val, { type: 'number', stored: true });
591
+ //deprecated - only for compatibility reasons - will be removed in future versions
478
592
  state = await this.adapter.getState('collected.consumptionStart');
479
593
  this.stateCache.set('collected.consumptionStart', state?.val, { type: 'number', stored: true });
480
594
  state = await this.adapter.getState('collected.dailyExternalYield');
595
+ //riemann sum - set start value and timestamp for correct computation of daily yield
481
596
  this.stateCache.set('collected.dailyExternalYield', state?.val, { type: 'number', stored: true });
482
597
  this.externalSum.setStart(state?.val, state?.ts);
598
+ state = await this.adapter.getState('collected.sumDaily.houseConsumption');
599
+ this.stateCache.set('collected.sumDaily.houseConsumption', state?.val, { type: 'number', stored: true });
600
+ this.houseConsumSum.setStart(state?.val, state?.ts);
601
+ state = await this.adapter.getState('collected.sumDaily.activePower');
602
+ this.stateCache.set('collected.sumDaily.activePower', state?.val, { type: 'number', stored: true });
603
+ this.actPowerSum.setStart(state?.val, state?.ts);
604
+ state = await this.adapter.getState('collected.sumDaily.inputPower');
605
+ this.stateCache.set('collected.sumDaily.inputPower', state?.val, { type: 'number', stored: true });
606
+ this.inPowerSum.setStart(state?.val, state?.ts);
607
+ state = await this.adapter.getState('collected.sumDaily.chargePower');
608
+ this.stateCache.set('collected.sumDaily.chargePower', state?.val, { type: 'number', stored: true });
609
+ this.chargeSum.setStart(state?.val, state?.ts);
610
+ state = await this.adapter.getState('collected.sumDaily.dischargePower');
611
+ this.stateCache.set('collected.sumDaily.dischargePower', state?.val, { type: 'number', stored: true });
612
+ this.dischargeSum.setStart(state?.val, state?.ts);
613
+ state = await this.adapter.getState('collected.sumDaily.feed-inPower');
614
+ this.stateCache.set('collected.sumDaily.feed-inPower', state?.val, { type: 'number', stored: true });
615
+ this.feedinSum.setStart(state?.val, state?.ts);
616
+ state = await this.adapter.getState('collected.sumDaily.feed-outPower');
617
+ this.stateCache.set('collected.sumDaily.feed-outPower', state?.val, { type: 'number', stored: true });
618
+ this.feedoutSum.setStart(state?.val, state?.ts);
483
619
  }
484
620
 
485
621
  //state
@@ -552,6 +688,7 @@ class Registers {
552
688
  this.externalSum.reset(); //reset for next day
553
689
  this.stateCache.set('collected.dailyExternalYield', 0, { type: 'number' });
554
690
  // copy consumption Sum to Start for the next day
691
+ // deprecated - consumption start is not used anymore, but we set it to the current consumption sum to avoid confusion
555
692
  this.stateCache.set('collected.consumptionStart', this.stateCache.get('collected.consumptionSum')?.value ?? 0, { type: 'number' });
556
693
  for (const device of this.adapter.devices) {
557
694
  if (device.instance.mitnightProcess) {
@@ -560,6 +697,17 @@ class Registers {
560
697
  }
561
698
  this.storeStates();
562
699
  }
700
+
701
+ destroy() {
702
+ for (const device of this.adapter.devices) {
703
+ if (typeof device.instance.destroy === 'function') {
704
+ device.instance.destroy();
705
+ }
706
+ }
707
+ if (typeof this.statistics.destroy === 'function') {
708
+ this.statistics.destroy();
709
+ }
710
+ }
563
711
  }
564
712
 
565
713
  module.exports = Registers;
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * breakdownChartHelper.js
5
+ *
6
+ * Provides all eCharts-specific helpers related to the consumption breakdown feature.
7
+ *
8
+ * Responsibilities:
9
+ * - Building eCharts series objects for breakdown entries (stacked bars in the
10
+ * lower consumption grid: xAxisIndex 1 / yAxisIndex 2)
11
+ * - Merging breakdown series names into the chart legend array
12
+ * - Replacing breakdown data placeholders in chart template strings
13
+ * (%%targetPath%% → serialised data array)
14
+ *
15
+ * This module has no knowledge of ioBroker states, the stateCache, or scheduling.
16
+ * It receives everything it needs as arguments, which makes it easy to unit-test.
17
+ */
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Fallback colour palette applied when a breakdown entry does not specify a colour.
25
+ * Cycles through the list if more entries exist than colours.
26
+ */
27
+ const DEFAULT_COLORS = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#03a9f4', '#009688', '#8bc34a', '#ff5722', '#795548', '#607d8b'];
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Public API
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Builds an array of eCharts series configurations for the given breakdown entries.
35
+ *
36
+ * Each series is rendered as a stacked bar in the lower consumption sub-grid
37
+ * (xAxisIndex 1, yAxisIndex 2) with stack key "consumptionBreakdown", so all
38
+ * breakdown bars pile on top of one another.
39
+ *
40
+ * @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
41
+ * The currently active breakdown entries (from ConsumptionBreakdown.entries).
42
+ * @param {string} [seriesType]
43
+ * eCharts series type passed in from the caller ('bar' or 'line').
44
+ * @param options - additional options to merge into each series config (e.g. for line styling)
45
+ * @param {string} valueFormatter string
46
+ * @returns {Array} eCharts series array (may be empty when entries is empty)
47
+ */
48
+ function buildBreakdownSeries(entries, seriesType = 'bar', options = {}, valueFormatter) {
49
+ if (!entries || entries.length === 0) return [];
50
+
51
+ return entries.map((bd, idx) => {
52
+ const color = bd.color || DEFAULT_COLORS[idx % DEFAULT_COLORS.length];
53
+ //const unit = bd.unit || 'kWh';
54
+
55
+ return {
56
+ name: bd.name,
57
+ type: seriesType,
58
+ stack: 'consumptionBreakdown', // same stack key as the Consumption series
59
+ xAxisIndex: 1,
60
+ yAxisIndex: 2,
61
+ data: `%%${bd.targetPath}%%`, // ensure it's an array (empty if undefined)
62
+ itemStyle: { color },
63
+ emphasis: { focus: 'series' },
64
+ //tooltip: { valueFormatter: value => `${value} ${unit}` },
65
+ tooltip: { valueFormatter: valueFormatter },
66
+ ...options,
67
+ };
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Merges breakdown series names into an existing legend data array.
73
+ *
74
+ * Appends names that are not already present; mutates and returns the array.
75
+ *
76
+ * @param {string[]} legendData
77
+ * Existing legend labels (built-in series names).
78
+ * @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
79
+ * Active breakdown entries whose names should appear in the legend.
80
+ * @returns {string[]} The same legendData array with breakdown names appended.
81
+ */
82
+ function mergeBreakdownLegend(legendData, entries) {
83
+ if (!entries || entries.length === 0) return legendData;
84
+
85
+ for (const bd of entries) {
86
+ if (!legendData.includes(bd.name)) {
87
+ legendData.push(bd.name);
88
+ }
89
+ }
90
+ return legendData;
91
+ }
92
+
93
+ /**
94
+ * Replaces breakdown data placeholders inside a serialised chart string.
95
+ *
96
+ * The template may contain placeholder tokens of the form '%%targetPath%%'
97
+ * (with surrounding single-quotes, as produced by javascript-stringify).
98
+ * Each is replaced with the JSON-serialised numeric data array.
99
+ *
100
+ * This function is intentionally a string-level operation so it works both
101
+ * for custom user templates (loaded from the state) and for the built-in
102
+ * template produced by _buildDefaultTemplate().
103
+ *
104
+ * @param {string} chartStr
105
+ * The chart configuration as a raw string (output of stringify()).
106
+ * @param {import('./consumptionBreakdown').ConsumptionBreakdownEntry[]} entries
107
+ * Active breakdown entries.
108
+ * @param {Record<string, number[]>} seriesData
109
+ * Map of targetPath → data array.
110
+ * @returns {string} The chart string with placeholders replaced.
111
+ */
112
+ function replacePlaceholders(chartStr, entries, seriesData) {
113
+ if (!entries || entries.length === 0) return chartStr;
114
+
115
+ for (const bd of entries) {
116
+ const placeholder = `'%%${bd.targetPath}%%'`;
117
+ const value = JSON.stringify(seriesData[bd.targetPath] ?? []);
118
+ chartStr = chartStr.replaceAll(placeholder, value);
119
+ }
120
+ return chartStr;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Exports
125
+ // ---------------------------------------------------------------------------
126
+
127
+ module.exports = {
128
+ buildBreakdownSeries,
129
+ mergeBreakdownLegend,
130
+ replacePlaceholders,
131
+ };