• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

naver / billboard.js / 27122665529

08 Jun 2026 07:32AM UTC coverage: 92.54% (-1.1%) from 93.621%
27122665529

push

github

web-flow
feat(canvas): add canvas rendering mode

Add canvas entry, renderer engine, axis renderer, theme probing, and hit detection.
Support canvas flow, subchart, zoom, selection, grid/regions, export, tooltip, and focus.
Add tests, benchmarks, types, and docs for canvas mode limitations.

10455 of 11840 branches covered (88.3%)

Branch coverage included in aggregate %.

5094 of 5363 new or added lines in 68 files covered. (94.98%)

19 existing lines in 3 files now uncovered.

13349 of 13883 relevant lines covered (96.15%)

26556.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.31
/src/ChartInternal/data/data.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 */
5
import {select as d3Select} from "d3-selection";
6
import {$BAR, $CANDLESTICK, $COMMON} from "../../config/classes";
7
import {KEY} from "../../module/Cache";
8
import {
9
        findIndex,
10
        getScrollPosition,
11
        getTransformCTM,
12
        getUnique,
13
        hasValue,
14
        hasViewBox,
15
        isArray,
16
        isBoolean,
17
        isDefined,
18
        isFunction,
19
        isNumber,
20
        isObject,
21
        isObjectType,
22
        isString,
23
        isUndefined,
24
        isValue,
25
        mergeArray,
26
        notEmpty,
27
        parseDate,
28
        sortValue
29
} from "../../module/util";
30
import type {IData, IDataPoint, IDataRow} from "./IData";
31

32
export default {
33
        isX(key) {
34
                const $$ = this;
17,106✔
35
                const {config} = $$;
17,106✔
36
                const dataKey = config.data_x && key === config.data_x;
17,106✔
37
                const existValue = notEmpty(config.data_xs) && hasValue(config.data_xs, key);
17,106✔
38

39
                return dataKey || existValue;
17,106✔
40
        },
41

42
        isStackNormalized(): boolean {
43
                const {config} = this;
109,845✔
44

45
                return !!(
109,845✔
46
                        (config.data_stack_normalize === true ||
47
                                isObjectType(config.data_stack_normalize)) &&
48
                        config.data_groups.length
49
                );
50
        },
51

52
        /**
53
         * Check if stack normalization should be applied per group
54
         * @returns {boolean}
55
         * @private
56
         */
57
        isStackNormalizedPerGroup(): boolean {
58
                const {config} = this;
8,520✔
59

60
                return !!(
8,520✔
61
                        isObjectType(config.data_stack_normalize) &&
9,210✔
62
                        config.data_stack_normalize?.perGroup &&
63
                        config.data_groups.length
64
                );
65
        },
66

67
        /**
68
         * Check if given id is grouped data or has grouped data
69
         * @param {string} id Data id value
70
         * @returns {boolean} is grouped data or has grouped data
71
         * @private
72
         */
73
        isGrouped(id?: string): boolean {
74
                const groups = this.config.data_groups;
874,474✔
75

76
                return id ? groups.some(v => v.indexOf(id) >= 0 && v.length > 1) : groups.length > 0;
874,474!
77
        },
78

79
        /**
80
         * Check if the given axis has any grouped data
81
         * @param {string} axisId Axis ID (e.g., "y", "y2")
82
         * @returns {boolean} true if axis has grouped data
83
         * @private
84
         */
85
        hasAxisGroupedData(axisId: "y" | "y2"): boolean {
86
                const $$ = this;
8,007✔
87
                const {axis} = $$;
8,007✔
88
                const targets = $$.data.targets;
8,007✔
89

90
                // Get all data IDs that belong to this axis
91
                const axisDataIds = targets
8,007✔
92
                        .filter(t => axis.getId(t.id) === axisId)
16,047✔
93
                        .map(t => t.id);
14,352✔
94

95
                // Check if any of the axis data IDs are in groups
96
                return axisDataIds.some(id => $$.isGrouped(id));
11,763✔
97
        },
98

99
        getXKey(id) {
100
                const $$ = this;
31,116✔
101
                const {config} = $$;
31,116✔
102

103
                return config.data_x ?
31,116✔
104
                        config.data_x :
105
                        (notEmpty(config.data_xs) ? config.data_xs[id] : null);
23,550✔
106
        },
107

108
        getXValuesOfXKey(key, targets) {
109
                const $$ = this;
3✔
110
                const ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : [];
3!
111
                let xValues;
112

113
                ids.forEach(id => {
3✔
114
                        if ($$.getXKey(id) === key) {
6✔
115
                                xValues = $$.data.xs[id];
3✔
116
                        }
117
                });
118

119
                return xValues;
3✔
120
        },
121

122
        /**
123
         * Get index number based on given x Axis value
124
         * @param {Date|number|string} x x Axis to be compared
125
         * @param {Array} basedX x Axis list to be based on
126
         * @returns {number} index number
127
         * @private
128
         */
129
        getIndexByX(x: Date | number | string, basedX: (Date | number | string)[]): number {
130
                const $$ = this;
285✔
131

132
                return basedX ?
285!
133
                        basedX.indexOf(isString(x) ? x : +x) :
×
134
                        ($$.filterByX($$.data.targets, x)[0] || {index: null}).index;
288✔
135
        },
136

137
        getXValue(id: string, i: number): number {
138
                const $$ = this;
192✔
139

140
                return id in $$.data.xs &&
192✔
141
                                $$.data.xs[id] &&
142
                                isValue($$.data.xs[id][i]) ?
143
                        $$.data.xs[id][i] :
144
                        i;
145
        },
146

147
        getOtherTargetXs(): string | null {
148
                const $$ = this;
33✔
149
                const idsForX = Object.keys($$.data.xs);
33✔
150

151
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
33!
152
        },
153

154
        getOtherTargetX(index: number): string | null {
155
                const xs = this.getOtherTargetXs();
27✔
156

157
                return xs && index < xs.length ? xs[index] : null;
27!
158
        },
159

160
        addXs(xs): void {
161
                const $$ = this;
6✔
162
                const {config} = $$;
6✔
163

164
                Object.keys(xs).forEach(id => {
6✔
165
                        config.data_xs[id] = xs[id];
6✔
166
                });
167
        },
168

169
        /**
170
         * Determine if x axis is multiple
171
         * @returns {boolean} true: multiple, false: single
172
         * @private
173
         */
174
        isMultipleX(): boolean {
175
                return !this.config.axis_x_forceAsSingle && (
28,842✔
176
                        notEmpty(this.config.data_xs) ||
177
                        this.hasType("bubble") ||
178
                        this.hasType("scatter")
179
                );
180
        },
181

182
        addName(data) {
183
                const $$ = this;
3,294✔
184
                const {config} = $$;
3,294✔
185
                let name;
186

187
                if (data) {
3,294✔
188
                        name = config.data_names[data.id];
3,273✔
189
                        data.name = name !== undefined ? name : data.id;
3,273!
190
                }
191

192
                return data;
3,294✔
193
        },
194

195
        /**
196
         * Get all values on given index
197
         * @param {number} index Index
198
         * @param {boolean} filterNull Filter nullish value
199
         * @returns {Array}
200
         * @private
201
         */
202
        getAllValuesOnIndex(index: number, filterNull = false) {
1,422✔
203
                const $$ = this;
1,482✔
204

205
                let value = $$.filterTargetsToShow($$.data.targets)
1,482✔
206
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
2,988✔
207

208
                if (filterNull) {
1,482✔
209
                        value = value.filter(v => v && "value" in v && isValue(v.value));
180✔
210
                }
211

212
                return value;
1,482✔
213
        },
214

215
        getValueOnIndex(values, index: number) {
216
                // Fast path: values are sorted by index from convertDataToTargets
217
                if (values[index]?.index === index) {
3,078✔
218
                        return values[index];
2,985✔
219
                }
220

221
                // Fallback for sparse/reordered data
222
                const valueOnIndex = values.filter(v => v.index === index);
321✔
223

224
                return valueOnIndex.length ? valueOnIndex[0] : null;
93✔
225
        },
226

227
        updateTargetX(targets, x) {
228
                const $$ = this;
9✔
229

230
                targets.forEach(t => {
9✔
231
                        t.values.forEach((v, i) => {
12✔
232
                                v.x = $$.generateTargetX(x[i], t.id, i);
36✔
233
                        });
234

235
                        $$.data.xs[t.id] = x;
12✔
236
                });
237
        },
238

239
        updateTargetXs(targets, xs) {
240
                const $$ = this;
3✔
241

242
                targets.forEach(t => {
3✔
243
                        xs[t.id] && $$.updateTargetX([t], xs[t.id]);
6✔
244
                });
245
        },
246

247
        generateTargetX(rawX, id: string, index: number) {
248
                const $$ = this;
400,932✔
249
                const {axis} = $$;
400,932✔
250
                let x = axis?.isCategorized() ? index : (rawX || index);
400,932✔
251

252
                if (axis?.isTimeSeries()) {
400,932✔
253
                        const fn = parseDate.bind($$);
13,098✔
254

255
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
13,098✔
256
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
387,834✔
257
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
24,594✔
258
                }
259

260
                return x;
400,932✔
261
        },
262

263
        updateXs(values): void {
264
                if (values.length) {
6,165✔
265
                        this.axis.xs = values.map(v => v.x);
40,554✔
266
                }
267
        },
268

269
        getPrevX(i: number): number[] | null {
270
                const x = this.axis.xs[i - 1];
70,044✔
271

272
                return isDefined(x) ? x : null;
70,044✔
273
        },
274

275
        getNextX(i: number): number[] | null {
276
                const x = this.axis.xs[i + 1];
70,044✔
277

278
                return isDefined(x) ? x : null;
70,044✔
279
        },
280

281
        /**
282
         * Get base value isAreaRangeType
283
         * @param {object} data Data object
284
         * @returns {number}
285
         * @private
286
         */
287
        getBaseValue(data): number {
288
                const $$ = this;
1,726,688✔
289
                const {hasAxis} = $$.state;
1,726,688✔
290
                let {value} = data;
1,726,688✔
291

292
                // In case of area-range, data is given as: [low, mid, high] or {low, mid, high}
293
                // will take the 'mid' as the base value
294
                if (value && hasAxis) {
1,726,688✔
295
                        if ($$.isAreaRangeType(data)) {
1,631,273✔
296
                                value = $$.getRangedData(data, "mid");
5,169✔
297
                        } else if ($$.isBubbleZType(data)) {
1,626,104✔
298
                                value = $$.getBubbleZData(value, "y");
945✔
299
                        }
300
                }
301

302
                return value;
1,726,688✔
303
        },
304

305
        /**
306
         * Get min/max value from the data
307
         * @private
308
         * @param {Array} data array data to be evaluated
309
         * @returns {{min: {number}, max: {number}}}
310
         */
311
        getMinMaxValue(data): {min: number, max: number} {
312
                const getBaseValue = this.getBaseValue.bind(this);
429✔
313
                let min = Infinity;
429✔
314
                let max = -Infinity;
429✔
315

316
                const targets = data || this.data.targets.map(t => t.values);
429✔
317

318
                for (let i = 0; i < targets.length; i++) {
429✔
319
                        const v = targets[i];
1,071✔
320

321
                        for (let j = 0; j < v.length; j++) {
1,071✔
322
                                const val = getBaseValue(v[j]);
2,802✔
323

324
                                if (isNumber(val)) {
2,802✔
325
                                        if (val < min) min = val;
2,715✔
326
                                        if (val > max) max = val;
2,715✔
327
                                }
328
                        }
329
                }
330

331
                return {min, max};
429✔
332
        },
333

334
        /**
335
         * Get the min/max data
336
         * @private
337
         * @returns {{min: Array, max: Array}}
338
         */
339
        getMinMaxData(): {min: IDataRow[], max: IDataRow[]} {
340
                const $$ = this;
1,323✔
341
                const cacheKey = KEY.dataMinMax;
1,323✔
342
                let minMaxData = $$.cache.get(cacheKey);
1,323✔
343

344
                if (!minMaxData) {
1,323✔
345
                        const data = $$.data.targets.map(t => t.values);
1,053✔
346
                        const minMax = $$.getMinMaxValue(data);
423✔
347

348
                        const min: IDataRow[] = [];
423✔
349
                        const max: IDataRow[] = [];
423✔
350

351
                        // Cache the getFilteredDataByValue function calls
352
                        const {min: minVal, max: maxVal} = minMax;
423✔
353

354
                        data.forEach(v => {
423✔
355
                                const minData = $$.getFilteredDataByValue(v, minVal);
1,053✔
356
                                const maxData = $$.getFilteredDataByValue(v, maxVal);
1,053✔
357

358
                                if (minData.length) {
1,053✔
359
                                        for (let i = 0; i < minData.length; i++) {
441✔
360
                                                min.push(minData[i]);
444✔
361
                                        }
362
                                }
363

364
                                if (maxData.length) {
1,053✔
365
                                        for (let i = 0; i < maxData.length; i++) {
423✔
366
                                                max.push(maxData[i]);
423✔
367
                                        }
368
                                }
369
                        });
370

371
                        // update the cached data
372
                        $$.cache.add(cacheKey, minMaxData = {min, max});
423✔
373
                }
374

375
                return minMaxData;
1,323✔
376
        },
377

378
        /**
379
         * Get sum of data per index
380
         * @param {string} targetId Target ID to get total for (only for normalized stack per group)
381
         * @private
382
         * @returns {Array}
383
         */
384
        getTotalPerIndex(targetId?: string) {
385
                const $$ = this;
7,380✔
386
                const {config} = $$;
7,380✔
387
                const cacheKey = targetId ? `${KEY.dataTotalPerIndex}-${targetId}` : KEY.dataTotalPerIndex;
7,380✔
388
                let sum = $$.cache.get(cacheKey);
7,380✔
389

390
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
7,380!
391
                        sum = [];
540✔
392

393
                        // When normalize per group is enabled and targetId is provided,
394
                        // only sum data within the same group
395
                        let {targets} = $$.data;
540✔
396

397
                        if ($$.isStackNormalizedPerGroup() && targetId) {
540✔
398
                                // Find which group the target belongs to
399
                                const group = config.data_groups.find(g => g.indexOf(targetId) >= 0);
96✔
400

401
                                if (group) {
84✔
402
                                        // Only sum targets in the same group
403
                                        targets = targets.filter(t => group.indexOf(t.id) >= 0);
126✔
404
                                } else {
405
                                        // If target is not in any group, return null to indicate no normalization
406
                                        return null;
54✔
407
                                }
408
                        }
409

410
                        targets.forEach(row => {
486✔
411
                                row.values.forEach((v, i) => {
2,619✔
412
                                        if (!sum[i]) {
7,371✔
413
                                                sum[i] = 0;
2,211✔
414
                                        }
415

416
                                        sum[i] += ~~v.value;
7,371✔
417
                                });
418
                        });
419

420
                        $$.cache.add(cacheKey, sum);
486✔
421
                }
422

423
                return sum;
7,326✔
424
        },
425

426
        /**
427
         * Get total data sum
428
         * @param {boolean} subtractHidden Subtract hidden data from total
429
         * @returns {number}
430
         * @private
431
         */
432
        getTotalDataSum(subtractHidden) {
433
                const $$ = this;
3,594✔
434
                const cacheKey = KEY.dataTotalSum;
3,594✔
435
                let total = $$.cache.get(cacheKey);
3,594✔
436

437
                if (!isNumber(total)) {
3,594✔
438
                        total = $$.data.targets.reduce((acc, t) => {
519✔
439
                                return acc + t.values.reduce((sum, v) => sum + (v.value ?? 0), 0);
1,950✔
440
                        }, 0);
441

442
                        $$.cache.add(cacheKey, total);
519✔
443
                }
444

445
                if (subtractHidden) {
3,594✔
446
                        total -= $$.getHiddenTotalDataSum();
2,592✔
447
                }
448

449
                return total;
3,594✔
450
        },
451

452
        /**
453
         * Get total hidden data sum
454
         * @returns {number}
455
         * @private
456
         */
457
        getHiddenTotalDataSum() {
458
                const $$ = this;
2,592✔
459
                const {api, state: {hiddenTargetIds}} = $$;
2,592✔
460
                let total = 0;
2,592✔
461

462
                if (hiddenTargetIds.size) {
2,592✔
463
                        total = api.data.values.bind(api)([...hiddenTargetIds])
213✔
464
                                .reduce((p, c) => p + c);
12✔
465
                }
466

467
                return total;
2,592✔
468
        },
469

470
        /**
471
         * Get filtered data by value
472
         * @param {object} data Data
473
         * @param {number} value Value to be filtered
474
         * @returns {Array} filtered array data
475
         * @private
476
         */
477
        getFilteredDataByValue(data, value) {
478
                return data.filter(t => this.getBaseValue(t) === value);
5,442✔
479
        },
480

481
        /**
482
         * Return the max length of the data
483
         * @returns {number} max data length
484
         * @private
485
         */
486
        getMaxDataCount(): number {
487
                const {targets} = this.data;
12,495✔
488
                let max = 0;
12,495✔
489

490
                for (let i = 0; i < targets.length; i++) {
12,495✔
491
                        if (targets[i].values.length > max) {
27,471✔
492
                                max = targets[i].values.length;
12,516✔
493
                        }
494
                }
495

496
                return max;
12,495✔
497
        },
498

499
        getMaxDataCountTarget() {
500
                const $$ = this;
6,165✔
501
                const {cache, state} = $$;
6,165✔
502
                const cached = cache.get(KEY.maxDataCountTarget);
6,165✔
503

504
                if (cached && cached.generation === state.dataGeneration) {
6,165✔
505
                        return cached.value;
672✔
506
                }
507

508
                let target = $$.filterTargetsToShow() || [];
5,493!
509
                const length = target.length;
5,493✔
510
                const isInverted = $$.config.axis_x_inverted;
5,493✔
511

512
                if (length > 1) {
5,493✔
513
                        const allX: any[] = [];
3,333✔
514

515
                        for (let i = 0; i < target.length; i++) {
3,333✔
516
                                const values = target[i].values;
9,348✔
517

518
                                for (let j = 0; j < values.length; j++) {
9,348✔
519
                                        allX.push(values[j].x);
41,205✔
520
                                }
521
                        }
522

523
                        target = allX;
3,333✔
524

525
                        target = sortValue(getUnique(target))
3,333✔
526
                                .map((x, index, array) => ({
16,581✔
527
                                        x,
528
                                        index: isInverted ? array.length - index - 1 : index
16,581!
529
                                }));
530
                } else if (length) {
2,160✔
531
                        target = target[0].values.concat();
1,989✔
532
                }
533

534
                cache.add(KEY.maxDataCountTarget, {value: target, generation: state.dataGeneration});
5,493✔
535

536
                return target;
5,493✔
537
        },
538

539
        mapToIds(targets): string[] {
540
                return targets.map(d => d.id);
76,077✔
541
        },
542

543
        mapToTargetIds(ids?: string[] | string): string[] {
544
                const $$ = this;
7,098✔
545

546
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
7,098✔
547
        },
548

549
        hasTarget(targets, id): boolean {
550
                const ids = this.mapToIds(targets);
93✔
551

552
                for (let i = 0, val; (val = ids[i]); i++) {
93✔
553
                        if (val === id) {
195✔
554
                                return true;
90✔
555
                        }
556
                }
557

558
                return false;
3✔
559
        },
560

561
        isTargetToShow(targetId): boolean {
562
                return !this.state.hiddenTargetIds.has(targetId);
237,069✔
563
        },
564

565
        isLegendToShow(targetId): boolean {
566
                return !this.state.hiddenLegendIds.has(targetId);
41,889✔
567
        },
568

569
        getTargetsToShow(): any[] {
570
                const {state} = this;
30,894✔
571

572
                return state._targetsToShow ?? this.filterTargetsToShow();
30,894✔
573
        },
574

575
        filterTargetsToShow(targets?) {
576
                const $$ = this;
99,723✔
577

578
                // When called without arguments, use caching
579
                if (!targets) {
99,723✔
580
                        const {cache, data, state} = $$;
25,557✔
581
                        const cacheKey = KEY.filteredTargets;
25,557✔
582
                        const cached = cache.get(cacheKey);
25,557✔
583

584
                        // Return cached result if generation matches
585
                        if (cached && cached.generation === state.dataGeneration) {
25,557✔
586
                                return cached.value;
17,946✔
587
                        }
588

589
                        // Compute and cache result
590
                        const filtered = data.targets.filter(t => $$.isTargetToShow(t.id));
16,734✔
591

592
                        cache.add(cacheKey, {value: filtered, generation: state.dataGeneration});
7,611✔
593

594
                        return filtered;
7,611✔
595
                }
596

597
                // When called with custom targets, don't cache
598
                return targets.filter(t => $$.isTargetToShow(t.id));
185,823✔
599
        },
600

601
        mapTargetsToUniqueXs(targets) {
602
                const $$ = this;
14,712✔
603
                const {axis} = $$;
14,712✔
604
                let xs: any[] = [];
14,712✔
605

606
                if (targets?.length) {
14,712✔
607
                        xs = getUnique(
14,592✔
608
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
1,087,041✔
609
                        );
610

611
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
14,592✔
612
                }
613

614
                return sortValue(xs);
14,712✔
615
        },
616

617
        /**
618
         * Add to thetarget Ids
619
         * @param {string} type State's prop name
620
         * @param {Array|string} targetIds Target ids array
621
         * @private
622
         */
623
        addTargetIds(type: string, targetIds: string[] | string): void {
624
                const {state} = this;
273✔
625
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as string[];
273✔
626

627
                ids.forEach(v => state[type].add(v));
369✔
628
        },
629

630
        /**
631
         * Remove from the state target Ids
632
         * @param {string} type State's prop name
633
         * @param {Array|string} targetIds Target ids array
634
         * @private
635
         */
636
        removeTargetIds(type: string, targetIds: string[] | string): void {
637
                const {state} = this;
216✔
638
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as string[];
216!
639

640
                ids.forEach(v => state[type].delete(v));
432✔
641
        },
642

643
        addHiddenTargetIds(targetIds: string[]): void {
644
                this.addTargetIds("hiddenTargetIds", targetIds);
252✔
645
        },
646

647
        removeHiddenTargetIds(targetIds: string[]): void {
648
                this.removeTargetIds("hiddenTargetIds", targetIds);
99✔
649
        },
650

651
        addHiddenLegendIds(targetIds: string[]): void {
652
                this.addTargetIds("hiddenLegendIds", targetIds);
21✔
653
        },
654

655
        removeHiddenLegendIds(targetIds: string[]): void {
656
                this.removeTargetIds("hiddenLegendIds", targetIds);
117✔
657
        },
658

659
        getValuesAsIdKeyed(targets) {
660
                const $$ = this;
1,302✔
661
                const {hasAxis} = $$.state;
1,302✔
662
                const ys = {};
1,302✔
663
                const isMultipleX = $$.isMultipleX();
1,302✔
664

665
                let xIndexMap: Map<any, number> | null = null;
1,302✔
666

667
                if (isMultipleX) {
1,302✔
668
                        const cached = $$.cache.get(KEY.valuesXIndexMap);
36✔
669

670
                        if (cached && cached.generation === $$.state.dataGeneration) {
36!
UNCOV
671
                                xIndexMap = cached.value;
×
672
                        } else {
673
                                const xs = $$.mapTargetsToUniqueXs($$.data.targets)
36✔
674
                                        .map(v => (isString(v) ? v : +v));
132!
675

676
                                xIndexMap = new Map(xs.map((x, i) => [x, i]));
132✔
677
                                $$.cache.add(KEY.valuesXIndexMap, {
36✔
678
                                        value: xIndexMap,
679
                                        generation: $$.state.dataGeneration
680
                                });
681
                        }
682
                }
683

684
                targets.forEach(t => {
1,302✔
685
                        const data: any[] = [];
5,706✔
686

687
                        t.values
5,706✔
688
                                .filter(({value}) => isValue(value) || value === null)
16,755✔
689
                                .forEach(v => {
690
                                        let {value} = v;
16,755✔
691

692
                                        // exclude 'volume' value to correct mis domain calculation
693
                                        if (value !== null && $$.isCandlestickType(v)) {
16,755✔
694
                                                value = isArray(value) ?
24!
695
                                                        value.slice(0, 4) :
696
                                                        [value.open, value.high, value.low, value.close];
697
                                        }
698

699
                                        if (isArray(value)) {
16,755✔
700
                                                data.push(...value);
186✔
701
                                        } else if (isObject(value) && "high" in value) {
16,569!
UNCOV
702
                                                data.push(...Object.values(value));
×
703
                                        } else if ($$.isBubbleZType(v)) {
16,569!
UNCOV
704
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
×
705
                                        } else {
706
                                                if (isMultipleX && xIndexMap) {
16,569✔
707
                                                        // Use Map for O(1) lookup instead of getIndexByX which uses indexOf
708
                                                        const xKey = isString(v.x) ? v.x : +v.x;
228!
709
                                                        const index = xIndexMap.get(xKey);
228✔
710

711
                                                        if (index !== undefined) {
228!
712
                                                                data[index as number] = value;
228✔
713
                                                        }
714
                                                } else {
715
                                                        data.push(value);
16,341✔
716
                                                }
717
                                        }
718
                                });
719

720
                        ys[t.id] = data;
5,706✔
721
                });
722

723
                return ys;
1,302✔
724
        },
725

726
        hasMultiTargets(): boolean {
727
                return this.filterTargetsToShow().length > 1;
264✔
728
        },
729

730
        /**
731
         * Sort targets data
732
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
733
         * @param {Array} targetsValue Target value
734
         * @returns {Array}
735
         * @private
736
         */
737
        orderTargets(targetsValue: IData[]): IData[] {
738
                const $$ = this;
29,265✔
739
                const targets = targetsValue.slice();
29,265✔
740
                const fn = $$.getSortCompareFn();
29,265✔
741

742
                fn && targets.sort(fn);
29,265✔
743

744
                return targets;
29,265✔
745
        },
746

747
        /**
748
         * Get data.order compare function
749
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
750
         * @returns {function} compare function
751
         * @private
752
         */
753
        getSortCompareFn(isReversed = false): Function | null {
29,265✔
754
                const $$ = this;
30,231✔
755
                const {config} = $$;
30,231✔
756
                const order = config.data_order;
30,231✔
757
                const orderAsc = /asc/i.test(order);
30,231✔
758
                const orderDesc = /desc/i.test(order);
30,231✔
759
                let fn;
760

761
                if (orderAsc || orderDesc) {
30,231✔
762
                        const reducer = (p, c) => p + Math.abs(c.value);
329,784✔
763
                        const sum = v => (isNumber(v) ? v : (
126,252✔
764
                                "values" in v ? v.values.reduce(reducer, 0) : v.value
125,202✔
765
                        ));
766

767
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
29,559✔
768
                                const t1Sum = sum(t1);
63,126✔
769
                                const t2Sum = sum(t2);
63,126✔
770

771
                                return isReversed ?
63,126✔
772
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
30,711!
773
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
32,415✔
774
                        };
775
                } else if (isFunction(order)) {
672✔
776
                        fn = order.bind($$.api);
54✔
777
                }
778

779
                return fn || null;
30,231✔
780
        },
781

782
        filterByX(targets, x) {
783
                return this.getValuesByX(targets).get(this.getXCacheKey(x)) || [];
363✔
784
        },
785

786
        filterNullish(data) {
787
                const filter = v => isValue(v.value);
70,239✔
788

789
                return data ?
20,268!
790
                        data.filter(
791
                                v => "value" in v ? filter(v) : v.values.some(filter)
70,002✔
792
                        ) :
793
                        data;
794
        },
795

796
        filterRemoveNull(data) {
797
                return data.filter(d => isValue(this.getBaseValue(d)));
375✔
798
        },
799

800
        filterByXDomain(targets, xDomain) {
801
                return targets.map(t => ({
153✔
802
                        id: t.id,
803
                        id_org: t.id_org,
804
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,842✔
805
                }));
806
        },
807

808
        hasDataLabel() {
809
                const dataLabels = this.config.data_labels;
64,278✔
810

811
                return (isBoolean(dataLabels) && dataLabels) ||
64,278✔
812
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
813
        },
814

815
        /**
816
         * Determine if has null value
817
         * @param {Array} targets Data array to be evaluated
818
         * @returns {boolean}
819
         * @private
820
         */
821
        hasNullDataValue(targets: IDataRow[]): boolean {
822
                return targets.some(({value}) => value === null);
342✔
823
        },
824

825
        /**
826
         * Get data index from the event coodinates
827
         * @param {Event} event Event object
828
         * @returns {number}
829
         * @private
830
         */
831
        getDataIndexFromEvent(event): number {
832
                const $$ = this;
1,539✔
833
                const {
834
                        $el,
835
                        config,
836
                        state: {hasRadar, inputType, eventReceiver: {coords, rect}}
837
                } = $$;
1,539✔
838
                let index;
839

840
                if (hasRadar) {
1,539✔
841
                        let target = event.target;
33✔
842

843
                        // in case of multilined axis text
844
                        if (/tspan/i.test(target.tagName)) {
33!
845
                                target = target.parentNode;
×
846
                        }
847

848
                        const d: any = d3Select(target).datum();
33✔
849

850
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
33!
851
                } else {
852
                        const isRotated = config.axis_rotated;
1,506✔
853
                        const scrollPos = getScrollPosition($el.chart.node());
1,506✔
854

855
                        // get data based on the mouse coords
856
                        const e = inputType === "touch" && event.changedTouches ?
1,506✔
857
                                event.changedTouches[0] :
858
                                event;
859

860
                        let point = isRotated ? e.clientY + scrollPos.y : e.clientX + scrollPos.x;
1,506✔
861

862
                        if (hasViewBox($el.svg)) {
1,506✔
863
                                const pos = [point, 0];
39✔
864

865
                                isRotated && pos.reverse();
39✔
866
                                point = getTransformCTM($el.eventRect.node(), ...pos)[isRotated ? "y" : "x"];
39✔
867
                        } else {
868
                                point -= isRotated ? rect.top : rect.left;
1,467✔
869
                        }
870

871
                        index = findIndex(
1,506✔
872
                                coords,
873
                                point,
874
                                0,
875
                                coords.length - 1,
876
                                isRotated
877
                        );
878
                }
879

880
                return index;
1,539✔
881
        },
882

883
        getDataLabelLength(min: number, max: number, key: "width" | "height"): number[] {
884
                const $$ = this;
2,835✔
885
                const paddingCoef = 1.3;
2,835✔
886
                const values = [min, max].map(v => $$.dataLabelFormat()(v));
5,670✔
887

888
                if ($$.config.render_mode === "canvas" && !$$.$el.svg) {
2,835✔
889
                        const chart = $$.$el.chart?.node?.();
201✔
890
                        const doc = chart?.ownerDocument;
201✔
891
                        const svg = doc?.createElementNS("http://www.w3.org/2000/svg", "svg");
201✔
892

893
                        if (chart && svg) {
201!
894
                                const texts = values.map(value => {
201✔
895
                                        const text = doc.createElementNS("http://www.w3.org/2000/svg", "text");
402✔
896

897
                                        text.textContent = value;
402✔
898
                                        svg.appendChild(text);
402✔
899

900
                                        return text;
402✔
901
                                });
902

903
                                svg.style.cssText =
201✔
904
                                        "position:absolute;visibility:hidden;left:-10000px;top:-10000px;";
905
                                chart.appendChild(svg);
201✔
906

907
                                const lengths = texts.map(text => text.getBoundingClientRect()[key] * paddingCoef);
402✔
908

909
                                svg.remove();
201✔
910

911
                                return lengths;
201✔
912
                        }
913
                }
914

915
                return $$.getTextRect(
2,634✔
916
                        values
917
                )?.map((rect: DOMRect) => rect[key] * paddingCoef) || [0, 0];
3,978✔
918
        },
919

920
        isNoneArc(d) {
921
                return this.hasTarget(this.data.targets, d.id);
×
922
        },
923

924
        isArc(d) {
925
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
926
        },
927

928
        findSameXOfValues(values, index) {
929
                const targetX = values[index].x;
×
930
                const sames: any[] = [];
×
931
                let i;
932

933
                for (i = index - 1; i >= 0; i--) {
×
934
                        if (targetX !== values[i].x) {
×
935
                                break;
×
936
                        }
937

938
                        sames.push(values[i]);
×
939
                }
940

941
                for (i = index; i < values.length; i++) {
×
942
                        if (targetX !== values[i].x) {
×
943
                                break;
×
944
                        }
945

946
                        sames.push(values[i]);
×
947
                }
948

949
                return sames;
×
950
        },
951

952
        /**
953
         * Get normalized x value cache key.
954
         * @param {Date|number|string} x X value
955
         * @returns {number|string} Cache key value
956
         * @private
957
         */
958
        getXCacheKey(x: Date | number | string): number | string {
959
                return isString(x) ? x : +x;
288,081✔
960
        },
961

962
        /**
963
         * Get data rows grouped by x value.
964
         * @param {Array} targets Data targets
965
         * @returns {Map} X-value keyed data rows
966
         * @private
967
         */
968
        getValuesByX(targets): Map<number | string, IDataRow[]> {
969
                const $$ = this;
363✔
970
                const {cache, state} = $$;
363✔
971
                const targetKey = targets.map(t => {
363✔
972
                        const {values} = t;
729✔
973
                        const first = values[0];
729✔
974
                        const last = values[values.length - 1];
729✔
975

976
                        return `${t.id}:${values.length}:${first ? $$.getXCacheKey(first.x) : ""}:${
729!
977
                                last ? $$.getXCacheKey(last.x) : ""
729!
978
                        }`;
979
                }).join("|");
980
                const cached = cache.get(KEY.valuesByX);
363✔
981

982
                if (
363✔
983
                        cached &&
579✔
984
                        cached.generation === state.dataGeneration &&
985
                        cached.targetKey === targetKey
986
                ) {
987
                        return cached.value;
108✔
988
                }
989

990
                const valueMap = new Map<number | string, IDataRow[]>();
255✔
991

992
                for (let i = 0; i < targets.length; i++) {
255✔
993
                        const values = targets[i].values;
498✔
994

995
                        for (let j = 0; j < values.length; j++) {
498✔
996
                                const v = values[j];
2,415✔
997
                                const x = $$.getXCacheKey(v.x);
2,415✔
998
                                const rows = valueMap.get(x);
2,415✔
999

1000
                                rows ? rows.push(v) : valueMap.set(x, [v]);
2,415✔
1001
                        }
1002
                }
1003

1004
                cache.add(KEY.valuesByX, {
255✔
1005
                        generation: state.dataGeneration,
1006
                        targetKey,
1007
                        value: valueMap
1008
                });
1009

1010
                return valueMap;
255✔
1011
        },
1012

1013
        /**
1014
         * Get candidate values near the current pointer position.
1015
         * @param {Array} values Data values
1016
         * @param {Array} pos Pointer position
1017
         * @param {boolean} useSortedIndex Whether values are sorted target values
1018
         * @returns {Array} Candidate values
1019
         * @private
1020
         */
1021
        getClosestCandidates(values, pos: [number, number], useSortedIndex = true): IDataRow[] {
×
1022
                const $$ = this;
306✔
1023
                const {config, scale} = $$;
306✔
1024
                const len = values.length;
306✔
1025
                const first = values[0];
306✔
1026

1027
                if (!useSortedIndex || len < 200 || !first || !config.data_xSort) {
306!
1028
                        return values;
306✔
1029
                }
1030

NEW
1031
                const isBar = $$.isBarType(first.id);
×
NEW
1032
                const isCandle = $$.isCandlestickType(first.id);
×
NEW
1033
                const sensitivity = config.point_sensitivity;
×
1034

NEW
1035
                if (!(isBar || isCandle) && !isNumber(sensitivity)) {
×
NEW
1036
                        return values;
×
1037
                }
1038

NEW
1039
                const xScale = scale.zoom || scale.x;
×
NEW
1040
                const pointerX = pos[+config.axis_rotated];
×
NEW
1041
                const isAscending = xScale(values[0].x) <= xScale(values[len - 1].x);
×
NEW
1042
                let start = 0;
×
NEW
1043
                let end = len - 1;
×
1044

NEW
1045
                while (start < end) {
×
NEW
1046
                        const mid = (start + end) >> 1;
×
NEW
1047
                        const x = xScale(values[mid].x);
×
1048

NEW
1049
                        if (isAscending ? x < pointerX : x > pointerX) {
×
NEW
1050
                                start = mid + 1;
×
1051
                        } else {
NEW
1052
                                end = mid;
×
1053
                        }
1054
                }
1055

NEW
1056
                const candidates: IDataRow[] = [];
×
NEW
1057
                const visited = new Set<number>();
×
NEW
1058
                const add = (index: number) => {
×
NEW
1059
                        if (index >= 0 && index < len && !visited.has(index)) {
×
NEW
1060
                                visited.add(index);
×
NEW
1061
                                candidates.push(values[index]);
×
1062
                        }
1063
                };
NEW
1064
                const addSameX = (index: number) => {
×
NEW
1065
                        if (index < 0 || index >= len) {
×
NEW
1066
                                return;
×
1067
                        }
1068

NEW
1069
                        const x = $$.getXCacheKey(values[index].x);
×
NEW
1070
                        let i = index;
×
1071

NEW
1072
                        while (i >= 0 && $$.getXCacheKey(values[i].x) === x) {
×
NEW
1073
                                add(i--);
×
1074
                        }
1075

NEW
1076
                        i = index + 1;
×
NEW
1077
                        while (i < len && $$.getXCacheKey(values[i].x) === x) {
×
NEW
1078
                                add(i++);
×
1079
                        }
1080
                };
1081

NEW
1082
                if (isBar || isCandle) {
×
NEW
1083
                        for (let i = start - 2; i <= start + 2; i++) {
×
NEW
1084
                                addSameX(i);
×
1085
                        }
1086
                } else {
NEW
1087
                        const maxDx = sensitivity as number;
×
NEW
1088
                        const scan = (index: number, direction: number) => {
×
NEW
1089
                                for (let i = index; i >= 0 && i < len; i += direction) {
×
NEW
1090
                                        const v = values[i];
×
1091

NEW
1092
                                        if (Math.abs(xScale(v.x) - pointerX) > maxDx) {
×
NEW
1093
                                                break;
×
1094
                                        }
1095

NEW
1096
                                        add(i);
×
1097
                                }
1098
                        };
1099

NEW
1100
                        scan(start, 1);
×
NEW
1101
                        scan(start - 1, -1);
×
1102
                }
1103

NEW
1104
                return candidates;
×
1105
        },
1106

1107
        findClosestFromTargets(targets, pos: [number, number]): IDataRow | undefined {
1108
                const $$ = this;
117✔
1109
                const candidates: IDataRow[] = [];
117✔
1110

1111
                for (let i = 0; i < targets.length; i++) {
117✔
1112
                        const closest = $$.findClosest(targets[i].values, pos);
207✔
1113

1114
                        closest && candidates.push(closest);
207✔
1115
                }
1116

1117
                // decide closest point and return
1118
                return $$.findClosest(candidates, pos, false);
117✔
1119
        },
1120

1121
        findClosest(values, pos: [number, number], useSortedIndex = true): IDataRow | undefined {
192✔
1122
                const $$ = this;
306✔
1123
                const {$el: {main}} = $$;
306✔
1124
                const data = $$.getClosestCandidates(values, pos, useSortedIndex);
306✔
1125

1126
                let minDist;
1127
                let closest;
1128

1129
                // find mouseovering bar/candlestick and closest point in a single pass
1130
                // https://github.com/naver/billboard.js/issues/2434
1131
                for (let i = 0; i < data.length; i++) {
306✔
1132
                        const v = data[i];
1,023✔
1133

1134
                        if (!v || !isValue(v.value)) {
1,023✔
1135
                                continue;
18✔
1136
                        }
1137

1138
                        const isBar = $$.isBarType(v.id);
1,005✔
1139
                        const isCandle = $$.isCandlestickType(v.id);
1,005✔
1140

1141
                        if (isBar || isCandle) {
1,005✔
1142
                                const selector = isBar ?
30✔
1143
                                        `.${$BAR.chartBar}.${$COMMON.target}${
1144
                                                $$.getTargetSelectorSuffix(v.id)
1145
                                        } .${$BAR.bar}-${v.index}` :
1146
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${
1147
                                                $$.getTargetSelectorSuffix(v.id)
1148
                                        } .${$CANDLESTICK.candlestick}-${v.index} path`;
1149

1150
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
30✔
1151
                                        closest = v;
6✔
1152
                                }
1153
                        } else {
1154
                                const d = $$.dist(v as IDataPoint, pos);
975✔
1155
                                const sensitivity = $$.getPointSensitivity(v);
975✔
1156

1157
                                if (d < sensitivity && (minDist === undefined || d < minDist)) {
975✔
1158
                                        minDist = d;
213✔
1159
                                        closest = v;
213✔
1160
                                }
1161
                        }
1162
                }
1163

1164
                return closest;
306✔
1165
        },
1166

1167
        dist(data: IDataPoint, pos: [number, number]) {
1168
                const $$ = this;
1,101✔
1169
                const {config: {axis_rotated: isRotated}, scale} = $$;
1,101✔
1170
                const xIndex = +isRotated; // true: 1, false: 0
1,101✔
1171
                const yIndex = +!isRotated; // true: 0, false: 1
1,101✔
1172
                const y = $$.circleY(data, data.index);
1,101✔
1173
                const x = (scale.zoom || scale.x)(data.x);
1,101✔
1174

1175
                return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2));
1,101✔
1176
        },
1177

1178
        /**
1179
         * Convert data for step type
1180
         * @param {Array} values Object data values
1181
         * @returns {Array}
1182
         * @private
1183
         */
1184
        convertValuesToStep(values) {
1185
                const $$ = this;
654✔
1186
                const {axis, config} = $$;
654✔
1187
                const stepType = config.line_step_type;
654✔
1188
                const isCategorized = axis ? axis.isCategorized() : false;
654!
1189
                const converted = isArray(values) ? values.concat() : [values];
654!
1190

1191
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
654✔
1192
                        return values;
345✔
1193
                }
1194

1195
                // when all data are null, return empty array
1196
                // https://github.com/naver/billboard.js/issues/3124
1197
                if (converted.length) {
309!
1198
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
1199
                        const head = converted[0];
309✔
1200
                        const tail = converted[converted.length - 1];
309✔
1201
                        const {id} = head;
309✔
1202
                        let {x} = head;
309✔
1203

1204
                        // insert head
1205
                        converted.unshift({x: --x, value: head.value, id});
309✔
1206

1207
                        isCategorized && stepType === "step-after" &&
309✔
1208
                                converted.unshift({x: x - 1, value: head.value, id});
1209

1210
                        // append tail
1211
                        x = tail.x;
309✔
1212
                        converted.push({x: ++x, value: tail.value, id});
309✔
1213

1214
                        isCategorized && stepType === "step-before" &&
309✔
1215
                                converted.push({x: x + 1, value: tail.value, id});
1216
                }
1217

1218
                return converted;
309✔
1219
        },
1220

1221
        convertValuesToRange(values) {
1222
                const converted = isArray(values) ? values.concat() : [values];
×
1223
                const ranges: {x: string | number, id: string, value: number}[] = [];
×
1224

1225
                converted.forEach(range => {
×
1226
                        const {x, id} = range;
×
1227

1228
                        ranges.push({
×
1229
                                x,
1230
                                id,
1231
                                value: range.value[0]
1232
                        });
1233

1234
                        ranges.push({
×
1235
                                x,
1236
                                id,
1237
                                value: range.value[2]
1238
                        });
1239
                });
1240

1241
                return ranges;
×
1242
        },
1243

1244
        updateDataAttributes(name, attrs) {
1245
                const $$ = this;
36✔
1246
                const {config} = $$;
36✔
1247
                const current = config[`data_${name}`];
36✔
1248

1249
                if (isUndefined(attrs)) {
36✔
1250
                        return current;
12✔
1251
                }
1252

1253
                Object.keys(attrs).forEach(id => {
24✔
1254
                        current[id] = attrs[id];
39✔
1255
                });
1256

1257
                $$.redraw({withLegend: true});
24✔
1258

1259
                return current;
24✔
1260
        },
1261

1262
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
6,363!
1263
                const value = d?.value;
8,466✔
1264

1265
                if (isArray(value)) {
8,466✔
1266
                        if (type === "bar") {
5,592✔
1267
                                return value.reduce((a, c) => c - a);
24✔
1268
                        } else {
1269
                                // @ts-ignore
1270
                                const index = {
5,568✔
1271
                                        areaRange: ["high", "mid", "low"],
1272
                                        candlestick: ["open", "high", "low", "close", "volume"]
1273
                                }[type].indexOf(key);
1274

1275
                                return index >= 0 && value ? value[index] : undefined;
5,568!
1276
                        }
1277
                } else if (value && key) {
2,874✔
1278
                        return value[key];
939✔
1279
                }
1280

1281
                return value;
1,935✔
1282
        },
1283

1284
        /**
1285
         * Set ratio for grouped data
1286
         * @param {Array} data Data array
1287
         * @private
1288
         */
1289
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
1290
                const $$ = this;
2,184✔
1291
                const {config} = $$;
2,184✔
1292

1293
                // calculate ratio if grouped data exists
1294
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
2,184✔
1295
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
6,702✔
1296

1297
                        data.forEach(v => {
507✔
1298
                                "values" in v ? v.values.forEach(setter) : setter(v);
6,093✔
1299
                        });
1300
                }
1301
        },
1302

1303
        /**
1304
         * Get ratio value
1305
         * @param {string} type Ratio for given type
1306
         * @param {object} d Data value object
1307
         * @param {boolean} asPercent Convert the return as percent or not
1308
         * @returns {number} Ratio value
1309
         * @private
1310
         */
1311
        getRatio(type: "arc" | "index" | "radar" | "bar" | "treemap", d, asPercent = false): number {
6,972✔
1312
                const $$ = this;
14,352✔
1313
                const {config, state} = $$;
14,352✔
1314
                const api = $$.api;
14,352✔
1315
                let ratio = 0;
14,352✔
1316

1317
                if (d && api.data.shown().length) {
14,352✔
1318
                        ratio = d.ratio || d.value;
14,307✔
1319

1320
                        if (type === "arc") {
14,307✔
1321
                                // if has padAngle set, calculate rate based on value
1322
                                if ($$.pie.padAngle()()) {
2,133✔
1323
                                        ratio = d.value / $$.getTotalDataSum(true);
66✔
1324

1325
                                        // otherwise, based on the rendered angle value
1326
                                } else {
1327
                                        const gaugeArcLength = config.gauge_fullCircle ?
2,067✔
1328
                                                $$.getArcLength() :
1329
                                                $$.getStartingAngle() * -2;
1330
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
2,067✔
1331

1332
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
2,067✔
1333
                                }
1334
                        } else if (type === "index") {
12,174✔
1335
                                const dataValues = api.data.values.bind(api);
7,380✔
1336
                                const {hiddenTargetIds} = state;
7,380✔
1337

1338
                                // For normalized stack per group, get total per group
1339
                                let total = this.getTotalPerIndex(
7,380✔
1340
                                        $$.isStackNormalizedPerGroup() ? d.id : undefined
7,380✔
1341
                                );
1342

1343
                                // If total is null, the data is not in any group - don't normalize
1344
                                if (total === null) {
7,380✔
1345
                                        return ratio;
54✔
1346
                                }
1347

1348
                                if (hiddenTargetIds.size) {
7,326✔
1349
                                        // When normalized per group, only subtract hidden data from the same group
1350
                                        let hiddenIds: string[] = [...hiddenTargetIds];
567✔
1351

1352
                                        if ($$.isStackNormalizedPerGroup() && d.id) {
567!
1353
                                                const group = config.data_groups.find(g => g.indexOf(d.id) >= 0);
×
1354
                                                if (group) {
×
1355
                                                        // Only consider hidden IDs in the same group
1356
                                                        hiddenIds = hiddenIds.filter(id => group.indexOf(id) >= 0);
×
1357
                                                }
1358
                                        }
1359

1360
                                        if (hiddenIds.length) {
567!
1361
                                                let hiddenSum = dataValues(hiddenIds, false);
567✔
1362

1363
                                                if (hiddenSum.length) {
567✔
1364
                                                        hiddenSum = hiddenSum
159✔
1365
                                                                .reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));
144✔
1366

1367
                                                        total = total.map((v, i) => v - hiddenSum[i]);
387✔
1368
                                                }
1369
                                        }
1370
                                }
1371

1372
                                const divisor = total[d.index];
7,326✔
1373

1374
                                d.ratio = isNumber(d.value) && total && divisor ? d.value / divisor : 0;
7,326✔
1375

1376
                                ratio = d.ratio;
7,326✔
1377
                        } else if (type === "radar") {
4,794✔
1378
                                ratio = (
1,542✔
1379
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
1380
                                ) * config.radar_size_ratio;
1381
                        } else if (type === "bar") {
3,252✔
1382
                                const yScale = $$.getYScaleById.bind($$)(d.id);
1,758✔
1383
                                const max = yScale.domain().reduce((a, c) => c - a);
1,758✔
1384

1385
                                // when all data are 0, return 0
1386
                                ratio = max === 0 ? 0 : Math.abs(
1,758✔
1387
                                        $$.getRangedData(d, null, type) / max
1388
                                );
1389
                        } else if (type === "treemap") {
1,494!
1390
                                ratio /= $$.getTotalDataSum(true);
1,494✔
1391
                        }
1392
                }
1393

1394
                return asPercent && ratio ? ratio * 100 : ratio;
14,298✔
1395
        },
1396

1397
        /**
1398
         * Sort data index to be aligned with x axis.
1399
         * @param {Array} tickValues Tick array values
1400
         * @private
1401
         */
1402
        updateDataIndexByX(tickValues) {
1403
                const $$ = this;
6,165✔
1404

1405
                const tickValueMap = tickValues.reduce((out, tick, index) => {
6,165✔
1406
                        out[Number(tick.x)] = index;
40,554✔
1407
                        return out;
40,554✔
1408
                }, {});
1409

1410
                $$.data.targets.forEach(t => {
6,165✔
1411
                        t.values.forEach((value, valueIndex) => {
12,561✔
1412
                                let index = tickValueMap[Number(value.x)];
68,340✔
1413

1414
                                if (index === undefined) {
68,340✔
1415
                                        index = valueIndex;
852✔
1416
                                }
1417
                                value.index = index;
68,340✔
1418
                        });
1419
                });
1420
        },
1421

1422
        /**
1423
         * Determine if bubble has dimension data
1424
         * @param {object|Array} d data value
1425
         * @returns {boolean}
1426
         * @private
1427
         */
1428
        isBubbleZType(d): boolean {
1429
                const $$ = this;
2,421,680✔
1430

1431
                return $$.isBubbleType(d) && (
2,421,680!
1432
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1433
                        (isArray(d.value) && d.value.length >= 2)
1434
                );
1435
        },
1436

1437
        /**
1438
         * Determine if bar has ranged data
1439
         * @param {Array} d data value
1440
         * @returns {boolean}
1441
         * @private
1442
         */
1443
        isBarRangeType(d): boolean {
1444
                const $$ = this;
86,547✔
1445
                const {value} = d;
86,547✔
1446

1447
                return $$.isBarType(d) && isArray(value) && value.length >= 2 &&
86,547✔
1448
                        value.every(isNumber);
1449
        },
1450

1451
        /**
1452
         * Get data object by id
1453
         * @param {string} id data id
1454
         * @returns {object}
1455
         * @private
1456
         */
1457
        getDataById(id: string) {
1458
                const d = this.cache.get(id) || this.api.data(id);
24,879✔
1459

1460
                return d?.[0] ?? d;
24,879✔
1461
        }
1462
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc