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

naver / billboard.js / 30443109728

29 Jul 2026 10:17AM UTC coverage: 93.712% (-0.1%) from 93.823%
30443109728

push

github

web-flow
feat(subchart): support configurable subchart rendering

Add subchart.type and subchart.types for independent overview series rendering.
Support subchart x/y/y2 axis ticks, formats, culling, outer ticks, and text visibility.
Add brush.enabled=false hover behavior and continuous focus grid across main/subchart.
Project candlestick values for subchart bars, line, and area using stock chart semantics.
Align SVG/canvas subchart axis, grid, and interaction behavior with demos/tests.

Ref #4175

11530 of 12842 branches covered (89.78%)

Branch coverage included in aggregate %.

553 of 584 new or added lines in 20 files covered. (94.69%)

108 existing lines in 12 files now uncovered.

14417 of 14846 relevant lines covered (97.11%)

29561.24 hits per line

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

93.18
/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 {TYPE} from "../../config/const";
8
import {KEY} from "../../module/Cache";
9
import {
10
        findIndex,
11
        getScrollPosition,
12
        getTransformCTM,
13
        getUnique,
14
        hasValue,
15
        hasViewBox,
16
        isArray,
17
        isBoolean,
18
        isDefined,
19
        isFunction,
20
        isNumber,
21
        isObject,
22
        isObjectType,
23
        isString,
24
        isUndefined,
25
        isValue,
26
        mergeArray,
27
        notEmpty,
28
        parseDate,
29
        sortValue
30
} from "../../module/util";
31
import type {IData, IDataPoint, IDataRow} from "./IData";
32

33
// ranged data key-to-index lookup, used by getRangedData()
34
const rangedDataKeyIndex: Record<string, Record<string, number>> = {
255✔
35
        areaRange: {high: 0, mid: 1, low: 2},
36
        candlestick: {open: 0, high: 1, low: 2, close: 3, volume: 4}
37
};
38

39
/**
40
 * Normalize target ids to an array
41
 * @param {Array|string} targetIds Target ids
42
 * @returns {Array} Target ids array
43
 * @private
44
 */
45
function normalizeTargetIds(targetIds: string[] | string): string[] {
46
        return (isArray(targetIds) ? targetIds : [targetIds]) as string[];
534✔
47
}
48

49
export default {
50
        isX(key) {
51
                const $$ = this;
17,751✔
52
                const {config} = $$;
17,751✔
53
                const dataKey = config.data_x && key === config.data_x;
17,751✔
54
                const existValue = notEmpty(config.data_xs) && hasValue(config.data_xs, key);
17,751✔
55

56
                return dataKey || existValue;
17,751✔
57
        },
58

59
        isStackNormalized(): boolean {
60
                const {config} = this;
119,090✔
61

62
                return !!(
119,090✔
63
                        (config.data_stack_normalize === true ||
64
                                isObjectType(config.data_stack_normalize)) &&
65
                        config.data_groups.length
66
                );
67
        },
68

69
        /**
70
         * Check if stack normalization should be applied per group
71
         * @returns {boolean}
72
         * @private
73
         */
74
        isStackNormalizedPerGroup(): boolean {
75
                const {config} = this;
8,520✔
76

77
                return !!(
8,520✔
78
                        isObjectType(config.data_stack_normalize) &&
9,210✔
79
                        config.data_stack_normalize?.perGroup &&
80
                        config.data_groups.length
81
                );
82
        },
83

84
        /**
85
         * Check if given id is grouped data or has grouped data
86
         * @param {string} id Data id value
87
         * @returns {boolean} is grouped data or has grouped data
88
         * @private
89
         */
90
        isGrouped(id?: string): boolean {
91
                const groups = this.config.data_groups;
867,905✔
92

93
                return id ? groups.some(v => v.indexOf(id) >= 0 && v.length > 1) : groups.length > 0;
867,905!
94
        },
95

96
        /**
97
         * Check if the given axis has any grouped data
98
         * @param {string} axisId Axis ID (e.g., "y", "y2")
99
         * @returns {boolean} true if axis has grouped data
100
         * @private
101
         */
102
        hasAxisGroupedData(axisId: "y" | "y2"): boolean {
103
                const $$ = this;
8,406✔
104
                const {axis} = $$;
8,406✔
105
                const targets = $$.data.targets;
8,406✔
106

107
                // Get all data IDs that belong to this axis
108
                const axisDataIds = targets
8,406✔
109
                        .filter(t => axis.getId(t.id) === axisId)
16,782✔
110
                        .map(t => t.id);
15,015✔
111

112
                // Check if any of the axis data IDs are in groups
113
                return axisDataIds.some(id => $$.isGrouped(id));
12,420✔
114
        },
115

116
        getXKey(id) {
117
                const $$ = this;
32,280✔
118
                const {config} = $$;
32,280✔
119

120
                return config.data_x ?
32,280✔
121
                        config.data_x :
122
                        (notEmpty(config.data_xs) ? config.data_xs[id] : null);
24,510✔
123
        },
124

125
        getXValuesOfXKey(key, targets) {
126
                const $$ = this;
3✔
127
                const ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : [];
3!
128
                let xValues;
129

130
                ids.forEach(id => {
3✔
131
                        if ($$.getXKey(id) === key) {
6✔
132
                                xValues = $$.data.xs[id];
3✔
133
                        }
134
                });
135

136
                return xValues;
3✔
137
        },
138

139
        /**
140
         * Get index number based on given x Axis value
141
         * @param {Date|number|string} x x Axis to be compared
142
         * @param {Array} basedX x Axis list to be based on
143
         * @returns {number} index number
144
         * @private
145
         */
146
        getIndexByX(x: Date | number | string, basedX: (Date | number | string)[]): number {
147
                const $$ = this;
285✔
148

149
                return basedX ?
285!
150
                        basedX.indexOf(isString(x) ? x : +x) :
×
151
                        ($$.filterByX($$.data.targets, x)[0] || {index: null}).index;
288✔
152
        },
153

154
        getXValue(id: string, i: number): number {
155
                const $$ = this;
192✔
156

157
                return id in $$.data.xs &&
192✔
158
                                $$.data.xs[id] &&
159
                                isValue($$.data.xs[id][i]) ?
160
                        $$.data.xs[id][i] :
161
                        i;
162
        },
163

164
        getOtherTargetXs(): string | null {
165
                const $$ = this;
33✔
166
                const idsForX = Object.keys($$.data.xs);
33✔
167

168
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
33!
169
        },
170

171
        getOtherTargetX(index: number): string | null {
172
                const xs = this.getOtherTargetXs();
27✔
173

174
                return xs && index < xs.length ? xs[index] : null;
27!
175
        },
176

177
        addXs(xs): void {
178
                const $$ = this;
6✔
179
                const {config} = $$;
6✔
180

181
                Object.keys(xs).forEach(id => {
6✔
182
                        config.data_xs[id] = xs[id];
6✔
183
                });
184
        },
185

186
        /**
187
         * Determine if x axis is multiple
188
         * @returns {boolean} true: multiple, false: single
189
         * @private
190
         */
191
        isMultipleX(): boolean {
192
                return !this.config.axis_x_forceAsSingle && (
30,216✔
193
                        notEmpty(this.config.data_xs) ||
194
                        this.hasType("bubble") ||
195
                        this.hasType("scatter")
196
                );
197
        },
198

199
        addName(data) {
200
                const $$ = this;
3,363✔
201
                const {config} = $$;
3,363✔
202
                let name;
203

204
                if (data) {
3,363✔
205
                        name = config.data_names[data.id];
3,342✔
206
                        data.name = name !== undefined ? name : data.id;
3,342!
207
                }
208

209
                return data;
3,363✔
210
        },
211

212
        /**
213
         * Get all values on given index
214
         * @param {number} index Index
215
         * @param {boolean} filterNull Filter nullish value
216
         * @returns {Array}
217
         * @private
218
         */
219
        getAllValuesOnIndex(index: number, filterNull = false) {
1,440✔
220
                const $$ = this;
1,500✔
221

222
                let value = $$.filterTargetsToShow($$.data.targets)
1,500✔
223
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
3,024✔
224

225
                if (filterNull) {
1,500✔
226
                        value = value.filter(v => v && "value" in v && isValue(v.value));
180✔
227
                }
228

229
                return value;
1,500✔
230
        },
231

232
        getValueOnIndex(values, index: number) {
233
                // Fast path: values are sorted by index from convertDataToTargets
234
                if (values[index]?.index === index) {
3,114✔
235
                        return values[index];
3,021✔
236
                }
237

238
                // Fallback for sparse/reordered data
239
                const valueOnIndex = values.filter(v => v.index === index);
321✔
240

241
                return valueOnIndex.length ? valueOnIndex[0] : null;
93✔
242
        },
243

244
        updateTargetX(targets, x) {
245
                const $$ = this;
15✔
246

247
                targets.forEach(t => {
15✔
248
                        t.values.forEach((v, i) => {
18✔
249
                                v.x = $$.generateTargetX(x[i], t.id, i);
60✔
250
                        });
251

252
                        $$.data.xs[t.id] = x;
18✔
253
                });
254
        },
255

256
        updateTargetXs(targets, xs) {
257
                const $$ = this;
6✔
258

259
                targets.forEach(t => {
6✔
260
                        xs[t.id] && $$.updateTargetX([t], xs[t.id]);
12✔
261
                });
262
        },
263

264
        generateTargetX(rawX, id: string, index: number) {
265
                const $$ = this;
403,494✔
266
                const {axis} = $$;
403,494✔
267
                let x = axis?.isCategorized() ? index : (rawX || index);
403,494✔
268

269
                if (axis?.isTimeSeries()) {
403,494✔
270
                        const fn = parseDate.bind($$);
13,098✔
271

272
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
13,098✔
273
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
390,396✔
274
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
24,702✔
275
                }
276

277
                return x;
403,494✔
278
        },
279

280
        updateXs(values): void {
281
                if (values.length) {
6,378✔
282
                        this.axis.xs = values.map(v => v.x);
41,433✔
283
                }
284
        },
285

286
        getPrevX(i: number): number[] | null {
287
                const x = this.axis.xs[i - 1];
73,044✔
288

289
                return isDefined(x) ? x : null;
73,044✔
290
        },
291

292
        getNextX(i: number): number[] | null {
293
                const x = this.axis.xs[i + 1];
73,044✔
294

295
                return isDefined(x) ? x : null;
73,044✔
296
        },
297

298
        /**
299
         * Get base value isAreaRangeType
300
         * @param {object} data Data object
301
         * @returns {number}
302
         * @private
303
         */
304
        getBaseValue(data): number {
305
                const $$ = this;
1,758,055✔
306
                const {hasAxis} = $$.state;
1,758,055✔
307
                let {value} = data;
1,758,055✔
308

309
                // In case of area-range, data is given as: [low, mid, high] or {low, mid, high}
310
                // will take the 'mid' as the base value
311
                if (value && hasAxis) {
1,758,055✔
312
                        if ($$.isAreaRangeType(data)) {
1,661,401✔
313
                                value = $$.getRangedData(data, "mid");
4,845✔
314
                        } else if ($$.isBubbleZType(data)) {
1,656,556✔
315
                                value = $$.getBubbleZData(value, "y");
855✔
316
                        }
317
                }
318

319
                return value;
1,758,055✔
320
        },
321

322
        /**
323
         * Get min/max value from the data
324
         * @private
325
         * @param {Array} data array data to be evaluated
326
         * @returns {{min: {number}, max: {number}}}
327
         */
328
        getMinMaxValue(data): {min: number, max: number} {
329
                const getBaseValue = this.getBaseValue.bind(this);
429✔
330
                let min = Infinity;
429✔
331
                let max = -Infinity;
429✔
332

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

335
                for (let i = 0; i < targets.length; i++) {
429✔
336
                        const v = targets[i];
1,071✔
337

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

341
                                if (isNumber(val)) {
2,802✔
342
                                        if (val < min) min = val;
2,715✔
343
                                        if (val > max) max = val;
2,715✔
344
                                }
345
                        }
346
                }
347

348
                return {min, max};
429✔
349
        },
350

351
        /**
352
         * Get the min/max data
353
         * @private
354
         * @returns {{min: Array, max: Array}}
355
         */
356
        getMinMaxData(): {min: IDataRow[], max: IDataRow[]} {
357
                const $$ = this;
1,323✔
358
                const cacheKey = KEY.dataMinMax;
1,323✔
359
                let minMaxData = $$.cache.get(cacheKey);
1,323✔
360

361
                if (!minMaxData) {
1,323✔
362
                        const data = $$.data.targets.map(t => t.values);
1,053✔
363
                        const minMax = $$.getMinMaxValue(data);
423✔
364

365
                        const min: IDataRow[] = [];
423✔
366
                        const max: IDataRow[] = [];
423✔
367

368
                        // Cache the getFilteredDataByValue function calls
369
                        const {min: minVal, max: maxVal} = minMax;
423✔
370

371
                        data.forEach(v => {
423✔
372
                                const minData = $$.getFilteredDataByValue(v, minVal);
1,053✔
373
                                const maxData = $$.getFilteredDataByValue(v, maxVal);
1,053✔
374

375
                                if (minData.length) {
1,053✔
376
                                        for (let i = 0; i < minData.length; i++) {
441✔
377
                                                min.push(minData[i]);
444✔
378
                                        }
379
                                }
380

381
                                if (maxData.length) {
1,053✔
382
                                        for (let i = 0; i < maxData.length; i++) {
423✔
383
                                                max.push(maxData[i]);
423✔
384
                                        }
385
                                }
386
                        });
387

388
                        // update the cached data
389
                        $$.cache.add(cacheKey, minMaxData = {min, max});
423✔
390
                }
391

392
                return minMaxData;
1,323✔
393
        },
394

395
        /**
396
         * Get sum of data per index
397
         * @param {string} targetId Target ID to get total for (only for normalized stack per group)
398
         * @private
399
         * @returns {Array}
400
         */
401
        getTotalPerIndex(targetId?: string) {
402
                const $$ = this;
7,380✔
403
                const {config} = $$;
7,380✔
404
                const cacheKey = targetId ? `${KEY.dataTotalPerIndex}-${targetId}` : KEY.dataTotalPerIndex;
7,380✔
405
                let sum = $$.cache.get(cacheKey);
7,380✔
406

407
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
7,380!
408
                        sum = [];
540✔
409

410
                        // When normalize per group is enabled and targetId is provided,
411
                        // only sum data within the same group
412
                        let {targets} = $$.data;
540✔
413

414
                        if ($$.isStackNormalizedPerGroup() && targetId) {
540✔
415
                                // Find which group the target belongs to
416
                                const group = config.data_groups.find(g => g.indexOf(targetId) >= 0);
96✔
417

418
                                if (group) {
84✔
419
                                        // Only sum targets in the same group
420
                                        targets = targets.filter(t => group.indexOf(t.id) >= 0);
126✔
421
                                } else {
422
                                        // If target is not in any group, return null to indicate no normalization
423
                                        return null;
54✔
424
                                }
425
                        }
426

427
                        targets.forEach(row => {
486✔
428
                                row.values.forEach((v, i) => {
2,619✔
429
                                        if (!sum[i]) {
7,371✔
430
                                                sum[i] = 0;
2,211✔
431
                                        }
432

433
                                        sum[i] += isNumber(v.value) ? v.value : 0;
7,371✔
434
                                });
435
                        });
436

437
                        $$.cache.add(cacheKey, sum);
486✔
438
                }
439

440
                return sum;
7,326✔
441
        },
442

443
        /**
444
         * Get total data sum
445
         * @param {boolean} subtractHidden Subtract hidden data from total
446
         * @returns {number}
447
         * @private
448
         */
449
        getTotalDataSum(subtractHidden) {
450
                const $$ = this;
3,672✔
451
                const cacheKey = KEY.dataTotalSum;
3,672✔
452
                let total = $$.cache.get(cacheKey);
3,672✔
453

454
                if (!isNumber(total)) {
3,672✔
455
                        total = $$.data.targets.reduce((acc, t) => {
534✔
456
                                return acc + t.values.reduce((sum, v) => sum + (v.value ?? 0), 0);
2,010✔
457
                        }, 0);
458

459
                        $$.cache.add(cacheKey, total);
534✔
460
                }
461

462
                if (subtractHidden) {
3,672✔
463
                        total -= $$.getHiddenTotalDataSum();
2,670✔
464
                }
465

466
                return total;
3,672✔
467
        },
468

469
        /**
470
         * Get total hidden data sum
471
         * @returns {number}
472
         * @private
473
         */
474
        getHiddenTotalDataSum() {
475
                const $$ = this;
2,670✔
476
                const {api, state: {hiddenTargetIds}} = $$;
2,670✔
477
                let total = 0;
2,670✔
478

479
                if (hiddenTargetIds.size) {
2,670✔
480
                        total = api.data.values.bind(api)([...hiddenTargetIds])
213✔
481
                                .reduce((p, c) => p + c, 0);
225✔
482
                }
483

484
                return total;
2,670✔
485
        },
486

487
        /**
488
         * Get filtered data by value
489
         * @param {object} data Data
490
         * @param {number} value Value to be filtered
491
         * @returns {Array} filtered array data
492
         * @private
493
         */
494
        getFilteredDataByValue(data, value) {
495
                return data.filter(t => this.getBaseValue(t) === value);
5,442✔
496
        },
497

498
        /**
499
         * Return the max length of the data
500
         * @returns {number} max data length
501
         * @private
502
         */
503
        getMaxDataCount(): number {
504
                const {targets} = this.data;
13,280✔
505
                let max = 0;
13,280✔
506

507
                for (let i = 0; i < targets.length; i++) {
13,280✔
508
                        if (targets[i].values.length > max) {
28,973✔
509
                                max = targets[i].values.length;
13,301✔
510
                        }
511
                }
512

513
                return max;
13,280✔
514
        },
515

516
        getMaxDataCountTarget() {
517
                const $$ = this;
6,378✔
518
                const {cache, state} = $$;
6,378✔
519
                const cached = cache.get(KEY.maxDataCountTarget);
6,378✔
520

521
                if (cached && cached.generation === state.dataGeneration) {
6,378✔
522
                        return cached.value;
678✔
523
                }
524

525
                let target = $$.filterTargetsToShow() || [];
5,700!
526
                const length = target.length;
5,700✔
527
                const isInverted = $$.config.axis_x_inverted;
5,700✔
528

529
                if (length > 1) {
5,700✔
530
                        const allX: any[] = [];
3,474✔
531

532
                        for (let i = 0; i < target.length; i++) {
3,474✔
533
                                const values = target[i].values;
9,630✔
534

535
                                for (let j = 0; j < values.length; j++) {
9,630✔
536
                                        allX.push(values[j].x);
42,321✔
537
                                }
538
                        }
539

540
                        target = allX;
3,474✔
541

542
                        target = sortValue(getUnique(target))
3,474✔
543
                                .map((x, index, array) => ({
17,139✔
544
                                        x,
545
                                        index: isInverted ? array.length - index - 1 : index
17,139!
546
                                }));
547
                } else if (length) {
2,226✔
548
                        target = target[0].values.concat();
2,055✔
549
                }
550

551
                cache.add(KEY.maxDataCountTarget, {value: target, generation: state.dataGeneration});
5,700✔
552

553
                return target;
5,700✔
554
        },
555

556
        mapToIds(targets): string[] {
557
                return targets.map(d => d.id);
83,638✔
558
        },
559

560
        mapToTargetIds(ids?: string[] | string): string[] {
561
                const $$ = this;
7,482✔
562

563
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
7,482✔
564
        },
565

566
        hasTarget(targets, id): boolean {
567
                const ids = this.mapToIds(targets);
96✔
568

569
                for (let i = 0, val; (val = ids[i]); i++) {
96✔
570
                        if (val === id) {
204✔
571
                                return true;
93✔
572
                        }
573
                }
574

575
                return false;
3✔
576
        },
577

578
        isTargetToShow(targetId): boolean {
579
                return !this.state.hiddenTargetIds.has(targetId);
238,252✔
580
        },
581

582
        isLegendToShow(targetId): boolean {
583
                return !this.state.hiddenLegendIds.has(targetId);
44,421✔
584
        },
585

586
        getTargetsToShow(): any[] {
587
                const {state} = this;
32,426✔
588

589
                return state._targetsToShow ?? this.filterTargetsToShow();
32,426✔
590
        },
591

592
        filterTargetsToShow(targets?) {
593
                const $$ = this;
102,005✔
594

595
                // When called without arguments, use caching
596
                if (!targets) {
102,005✔
597
                        const {cache, data, state} = $$;
28,472✔
598
                        const cacheKey = KEY.filteredTargets;
28,472✔
599
                        const cached = cache.get(cacheKey);
28,472✔
600

601
                        // Return cached result if generation matches
602
                        if (cached && cached.generation === state.dataGeneration) {
28,472✔
603
                                return cached.value;
20,000✔
604
                        }
605

606
                        // Compute and cache result
607
                        const filtered = data.targets.filter(t => $$.isTargetToShow(t.id));
18,285✔
608

609
                        cache.add(cacheKey, {value: filtered, generation: state.dataGeneration});
8,472✔
610

611
                        return filtered;
8,472✔
612
                }
613

614
                // When called with custom targets, don't cache
615
                return targets.filter(t => $$.isTargetToShow(t.id));
183,748✔
616
        },
617

618
        mapTargetsToUniqueXs(targets) {
619
                const $$ = this;
15,810✔
620
                const {axis} = $$;
15,810✔
621
                let xs: any[] = [];
15,810✔
622

623
                if (targets?.length) {
15,810✔
624
                        xs = getUnique(
15,690✔
625
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
1,095,474✔
626
                        );
627

628
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
15,690✔
629
                }
630

631
                return sortValue(xs);
15,810✔
632
        },
633

634
        /**
635
         * Add to thetarget Ids
636
         * @param {string} type State's prop name
637
         * @param {Array|string} targetIds Target ids array
638
         * @private
639
         */
640
        addTargetIds(type: string, targetIds: string[] | string): void {
641
                const {state} = this;
291✔
642

643
                normalizeTargetIds(targetIds).forEach(v => state[type].add(v));
387✔
644
        },
645

646
        /**
647
         * Remove from the state target Ids
648
         * @param {string} type State's prop name
649
         * @param {Array|string} targetIds Target ids array
650
         * @private
651
         */
652
        removeTargetIds(type: string, targetIds: string[] | string): void {
653
                const {state} = this;
243✔
654

655
                normalizeTargetIds(targetIds).forEach(v => state[type].delete(v));
471✔
656
        },
657

658
        addHiddenTargetIds(targetIds: string[]): void {
659
                this.addTargetIds("hiddenTargetIds", targetIds);
267✔
660
        },
661

662
        removeHiddenTargetIds(targetIds: string[]): void {
663
                this.removeTargetIds("hiddenTargetIds", targetIds);
111✔
664
        },
665

666
        addHiddenLegendIds(targetIds: string[]): void {
667
                this.addTargetIds("hiddenLegendIds", targetIds);
24✔
668
        },
669

670
        removeHiddenLegendIds(targetIds: string[]): void {
671
                this.removeTargetIds("hiddenLegendIds", targetIds);
132✔
672
        },
673

674
        getValuesAsIdKeyed(targets) {
675
                const $$ = this;
1,329✔
676
                const {hasAxis} = $$.state;
1,329✔
677
                const ys = {};
1,329✔
678
                const isMultipleX = $$.isMultipleX();
1,329✔
679

680
                let xIndexMap: Map<any, number> | null = null;
1,329✔
681

682
                if (isMultipleX) {
1,329✔
683
                        const cached = $$.cache.get(KEY.valuesXIndexMap);
36✔
684

685
                        if (cached && cached.generation === $$.state.dataGeneration) {
36!
UNCOV
686
                                xIndexMap = cached.value;
×
687
                        } else {
688
                                const xs = $$.mapTargetsToUniqueXs($$.data.targets)
36✔
689
                                        .map(v => (isString(v) ? v : +v));
132!
690

691
                                xIndexMap = new Map(xs.map((x, i) => [x, i]));
132✔
692
                                $$.cache.add(KEY.valuesXIndexMap, {
36✔
693
                                        value: xIndexMap,
694
                                        generation: $$.state.dataGeneration
695
                                });
696
                        }
697
                }
698

699
                targets.forEach(t => {
1,329✔
700
                        const data: any[] = [];
5,796✔
701

702
                        t.values
5,796✔
703
                                .filter(({value}) => isValue(value) || value === null)
17,043✔
704
                                .forEach(v => {
705
                                        let {value} = v;
17,043✔
706

707
                                        // exclude 'volume' value to correct mis domain calculation
708
                                        if (value !== null && $$.isCandlestickType(v)) {
17,043✔
709
                                                value = isArray(value) ?
24!
710
                                                        value.slice(0, 4) :
711
                                                        [value.open, value.high, value.low, value.close];
712
                                        }
713

714
                                        if (isArray(value)) {
17,043✔
715
                                                data.push(...value);
186✔
716
                                        } else if (isObject(value) && "high" in value) {
16,857!
UNCOV
717
                                                data.push(...Object.values(value));
×
718
                                        } else if ($$.isBubbleZType(v)) {
16,857!
UNCOV
719
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
×
720
                                        } else {
721
                                                if (isMultipleX && xIndexMap) {
16,857✔
722
                                                        // Use Map for O(1) lookup instead of getIndexByX which uses indexOf
723
                                                        const xKey = isString(v.x) ? v.x : +v.x;
228!
724
                                                        const index = xIndexMap.get(xKey);
228✔
725

726
                                                        if (index !== undefined) {
228!
727
                                                                data[index as number] = value;
228✔
728
                                                        }
729
                                                } else {
730
                                                        data.push(value);
16,629✔
731
                                                }
732
                                        }
733
                                });
734

735
                        ys[t.id] = data;
5,796✔
736
                });
737

738
                return ys;
1,329✔
739
        },
740

741
        hasMultiTargets(): boolean {
742
                return this.filterTargetsToShow().length > 1;
264✔
743
        },
744

745
        /**
746
         * Sort targets data
747
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
748
         * @param {Array} targetsValue Target value
749
         * @returns {Array}
750
         * @private
751
         */
752
        orderTargets(targetsValue: IData[]): IData[] {
753
                const $$ = this;
32,789✔
754
                const targets = targetsValue.slice();
32,789✔
755
                const fn = $$.getSortCompareFn();
32,789✔
756

757
                fn && targets.sort(fn);
32,789✔
758

759
                return targets;
32,789✔
760
        },
761

762
        /**
763
         * Get data.order compare function
764
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
765
         * @returns {function} compare function
766
         * @private
767
         */
768
        getSortCompareFn(isReversed = false): Function | null {
32,789✔
769
                const $$ = this;
33,785✔
770
                const {config} = $$;
33,785✔
771
                const order = config.data_order;
33,785✔
772
                const orderAsc = /asc/i.test(order);
33,785✔
773
                const orderDesc = /desc/i.test(order);
33,785✔
774
                let fn;
775

776
                if (orderAsc || orderDesc) {
33,785✔
777
                        const reducer = (p, c) => p + Math.abs(c.value);
294,840✔
778
                        const sum = v => (isNumber(v) ? v : (
84,270✔
779
                                "values" in v ? v.values.reduce(reducer, 0) : v.value
83,220✔
780
                        ));
781

782
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
33,113✔
783
                                const t1Sum = sum(t1);
42,135✔
784
                                const t2Sum = sum(t2);
42,135✔
785

786
                                return isReversed ?
42,135✔
787
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
7,386!
788
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
34,749✔
789
                        };
790
                } else if (isFunction(order)) {
672✔
791
                        fn = order.bind($$.api);
54✔
792
                }
793

794
                return fn || null;
33,785✔
795
        },
796

797
        filterByX(targets, x) {
798
                return this.getValuesByX(targets).get(this.getXCacheKey(x)) || [];
375✔
799
        },
800

801
        filterNullish(data) {
802
                const filter = v => isValue(v.value);
72,144✔
803

804
                return data ?
20,994!
805
                        data.filter(
806
                                v => "value" in v ? filter(v) : v.values.some(filter)
71,907✔
807
                        ) :
808
                        data;
809
        },
810

811
        filterRemoveNull(data) {
812
                return data.filter(d => isValue(this.getBaseValue(d)));
276✔
813
        },
814

815
        filterByXDomain(targets, xDomain) {
816
                return targets.map(t => ({
153✔
817
                        id: t.id,
818
                        id_org: t.id_org,
819
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,842✔
820
                }));
821
        },
822

823
        hasDataLabel() {
824
                const dataLabels = this.config.data_labels;
74,989✔
825

826
                return (isBoolean(dataLabels) && dataLabels) ||
74,989✔
827
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
828
        },
829

830
        /**
831
         * Determine if has null value
832
         * @param {Array} targets Data array to be evaluated
833
         * @returns {boolean}
834
         * @private
835
         */
836
        hasNullDataValue(targets: IDataRow[]): boolean {
837
                return targets.some(({value}) => value === null);
342✔
838
        },
839

840
        /**
841
         * Get data index from the event coodinates
842
         * @param {Event} event Event object
843
         * @returns {number}
844
         * @private
845
         */
846
        getDataIndexFromEvent(event): number {
847
                const $$ = this;
1,557✔
848
                const {
849
                        $el,
850
                        config,
851
                        state: {hasRadar, inputType, eventReceiver: {coords, rect}}
852
                } = $$;
1,557✔
853
                let index;
854

855
                if (hasRadar) {
1,557✔
856
                        let target = event.target;
33✔
857

858
                        // in case of multilined axis text
859
                        if (/tspan/i.test(target.tagName)) {
33!
UNCOV
860
                                target = target.parentNode;
×
861
                        }
862

863
                        const d: any = d3Select(target).datum();
33✔
864

865
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
33!
866
                } else {
867
                        const isRotated = config.axis_rotated;
1,524✔
868
                        const scrollPos = getScrollPosition($el.chart.node());
1,524✔
869

870
                        // get data based on the mouse coords
871
                        const e = inputType === "touch" && event.changedTouches ?
1,524✔
872
                                event.changedTouches[0] :
873
                                event;
874

875
                        let point = isRotated ? e.clientY + scrollPos.y : e.clientX + scrollPos.x;
1,524✔
876

877
                        if (hasViewBox($el.svg)) {
1,524✔
878
                                const pos = [point, 0];
39✔
879

880
                                isRotated && pos.reverse();
39✔
881
                                point = getTransformCTM($el.eventRect.node(), ...pos)[isRotated ? "y" : "x"];
39✔
882
                        } else {
883
                                point -= isRotated ? rect.top : rect.left;
1,485✔
884
                        }
885

886
                        index = findIndex(
1,524✔
887
                                coords,
888
                                point,
889
                                0,
890
                                coords.length - 1,
891
                                isRotated
892
                        );
893
                }
894

895
                return index;
1,557✔
896
        },
897

898
        getDataLabelLength(min: number, max: number, key: "width" | "height"): number[] {
899
                const $$ = this;
2,895✔
900
                const paddingCoef = 1.3;
2,895✔
901
                const values = [min, max].map(v => $$.dataLabelFormat()(v));
5,790✔
902

903
                if ($$.config.render_mode === "canvas" && !$$.$el.svg) {
2,895✔
904
                        const chart = $$.$el.chart?.node?.();
237✔
905
                        const doc = chart?.ownerDocument;
237✔
906
                        const svg = doc?.createElementNS("http://www.w3.org/2000/svg", "svg");
237✔
907

908
                        if (chart && svg) {
237!
909
                                const texts = values.map(value => {
237✔
910
                                        const text = doc.createElementNS("http://www.w3.org/2000/svg", "text");
474✔
911

912
                                        text.textContent = value;
474✔
913
                                        svg.appendChild(text);
474✔
914

915
                                        return text;
474✔
916
                                });
917

918
                                svg.style.cssText =
237✔
919
                                        "position:absolute;visibility:hidden;left:-10000px;top:-10000px;";
920
                                chart.appendChild(svg);
237✔
921

922
                                const lengths = texts.map(text => text.getBoundingClientRect()[key] * paddingCoef);
474✔
923

924
                                svg.remove();
237✔
925

926
                                return lengths;
237✔
927
                        }
928
                }
929

930
                return $$.getTextRect(
2,658✔
931
                        values
932
                )?.map((rect: DOMRect) => rect[key] * paddingCoef) || [0, 0];
4,014✔
933
        },
934

935
        isNoneArc(d) {
936
                return this.hasTarget(this.data.targets, d.id);
×
937
        },
938

939
        isArc(d) {
940
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
941
        },
942

943
        findSameXOfValues(values, index) {
UNCOV
944
                const targetX = values[index].x;
×
945
                const sames: any[] = [];
×
946
                let i;
947

948
                for (i = index - 1; i >= 0; i--) {
×
949
                        if (targetX !== values[i].x) {
×
950
                                break;
×
951
                        }
952

953
                        sames.push(values[i]);
×
954
                }
955

956
                for (i = index; i < values.length; i++) {
×
UNCOV
957
                        if (targetX !== values[i].x) {
×
UNCOV
958
                                break;
×
959
                        }
960

UNCOV
961
                        sames.push(values[i]);
×
962
                }
963

UNCOV
964
                return sames;
×
965
        },
966

967
        /**
968
         * Get normalized x value cache key.
969
         * @param {Date|number|string} x X value
970
         * @returns {number|string} Cache key value
971
         * @private
972
         */
973
        getXCacheKey(x: Date | number | string): number | string {
974
                return isString(x) ? x : +x;
304,170✔
975
        },
976

977
        /**
978
         * Get data rows grouped by x value.
979
         * @param {Array} targets Data targets
980
         * @returns {Map} X-value keyed data rows
981
         * @private
982
         */
983
        getValuesByX(targets): Map<number | string, IDataRow[]> {
984
                const $$ = this;
375✔
985
                const {cache, state} = $$;
375✔
986
                const targetKey = targets.map(t => {
375✔
987
                        const {values} = t;
750✔
988
                        const first = values[0];
750✔
989
                        const last = values[values.length - 1];
750✔
990

991
                        return `${t.id}:${values.length}:${first ? $$.getXCacheKey(first.x) : ""}:${
750!
992
                                last ? $$.getXCacheKey(last.x) : ""
750!
993
                        }`;
994
                }).join("|");
995
                const cached = cache.get(KEY.valuesByX);
375✔
996

997
                if (
375✔
998
                        cached &&
591✔
999
                        cached.generation === state.dataGeneration &&
1000
                        cached.targetKey === targetKey
1001
                ) {
1002
                        return cached.value;
108✔
1003
                }
1004

1005
                const valueMap = new Map<number | string, IDataRow[]>();
267✔
1006

1007
                for (let i = 0; i < targets.length; i++) {
267✔
1008
                        const values = targets[i].values;
519✔
1009

1010
                        for (let j = 0; j < values.length; j++) {
519✔
1011
                                const v = values[j];
2,487✔
1012
                                const x = $$.getXCacheKey(v.x);
2,487✔
1013
                                const rows = valueMap.get(x);
2,487✔
1014

1015
                                rows ? rows.push(v) : valueMap.set(x, [v]);
2,487✔
1016
                        }
1017
                }
1018

1019
                cache.add(KEY.valuesByX, {
267✔
1020
                        generation: state.dataGeneration,
1021
                        targetKey,
1022
                        value: valueMap
1023
                });
1024

1025
                return valueMap;
267✔
1026
        },
1027

1028
        /**
1029
         * Get candidate values near the current pointer position.
1030
         * @param {Array} values Data values
1031
         * @param {Array} pos Pointer position
1032
         * @param {boolean} useSortedIndex Whether values are sorted target values
1033
         * @returns {Array} Candidate values
1034
         * @private
1035
         */
1036
        getClosestCandidates(values, pos: [number, number], useSortedIndex = true): IDataRow[] {
21✔
1037
                const $$ = this;
330✔
1038
                const {config, scale} = $$;
330✔
1039
                const len = values.length;
330✔
1040
                const first = values[0];
330✔
1041

1042
                if (!useSortedIndex || len < 200 || !first || !config.data_xSort) {
330✔
1043
                        return values;
315✔
1044
                }
1045

1046
                const isBar = $$.isBarType(first.id);
15✔
1047
                const isCandle = $$.isCandlestickType(first.id);
15✔
1048
                const sensitivity = config.point_sensitivity;
15✔
1049

1050
                if (!(isBar || isCandle) && !isNumber(sensitivity)) {
15✔
1051
                        return values;
3✔
1052
                }
1053

1054
                const xScale = scale.zoom || scale.x;
12✔
1055
                const pointerX = pos[+config.axis_rotated];
12✔
1056
                const isAscending = xScale(values[0].x) <= xScale(values[len - 1].x);
12✔
1057
                let start = 0;
12✔
1058
                let end = len - 1;
12✔
1059

1060
                while (start < end) {
12✔
1061
                        const mid = (start + end) >> 1;
96✔
1062
                        const x = xScale(values[mid].x);
96✔
1063

1064
                        if (isAscending ? x < pointerX : x > pointerX) {
96✔
1065
                                start = mid + 1;
42✔
1066
                        } else {
1067
                                end = mid;
54✔
1068
                        }
1069
                }
1070

1071
                const candidates: IDataRow[] = [];
12✔
1072
                const visited = new Set<number>();
12✔
1073
                const add = (index: number) => {
12✔
1074
                        if (index >= 0 && index < len && !visited.has(index)) {
87✔
1075
                                visited.add(index);
57✔
1076
                                candidates.push(values[index]);
57✔
1077
                        }
1078
                };
1079
                const addSameX = (index: number) => {
12✔
1080
                        if (index < 0 || index >= len) {
30✔
1081
                                return;
9✔
1082
                        }
1083

1084
                        const x = $$.getXCacheKey(values[index].x);
21✔
1085
                        let i = index;
21✔
1086

1087
                        while (i >= 0 && $$.getXCacheKey(values[i].x) === x) {
21✔
1088
                                add(i--);
30✔
1089
                        }
1090

1091
                        i = index + 1;
21✔
1092
                        while (i < len && $$.getXCacheKey(values[i].x) === x) {
21✔
1093
                                add(i++);
27✔
1094
                        }
1095
                };
1096

1097
                if (isBar || isCandle) {
12✔
1098
                        for (let i = start - 2; i <= start + 2; i++) {
6✔
1099
                                addSameX(i);
30✔
1100
                        }
1101
                } else {
1102
                        const maxDx = sensitivity as number;
6✔
1103
                        const scan = (index: number, direction: number) => {
6✔
1104
                                for (let i = index; i >= 0 && i < len; i += direction) {
12✔
1105
                                        const v = values[i];
42✔
1106

1107
                                        if (Math.abs(xScale(v.x) - pointerX) > maxDx) {
42✔
1108
                                                break;
12✔
1109
                                        }
1110

1111
                                        add(i);
30✔
1112
                                }
1113
                        };
1114

1115
                        scan(start, 1);
6✔
1116
                        scan(start - 1, -1);
6✔
1117
                }
1118

1119
                return candidates;
12✔
1120
        },
1121

1122
        findClosestFromTargets(targets, pos: [number, number]): IDataRow | undefined {
1123
                const $$ = this;
117✔
1124
                const candidates: IDataRow[] = [];
117✔
1125

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

1129
                        closest && candidates.push(closest);
207✔
1130
                }
1131

1132
                // decide closest point and return
1133
                return $$.findClosest(candidates, pos, false);
117✔
1134
        },
1135

1136
        findClosest(values, pos: [number, number], useSortedIndex = true): IDataRow | undefined {
192✔
1137
                const $$ = this;
306✔
1138
                const {$el: {main}} = $$;
306✔
1139
                const data = $$.getClosestCandidates(values, pos, useSortedIndex);
306✔
1140

1141
                let minDist;
1142
                let closest;
1143

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

1149
                        if (!v || !isValue(v.value)) {
1,023✔
1150
                                continue;
18✔
1151
                        }
1152

1153
                        const isBar = $$.isBarType(v.id);
1,005✔
1154
                        const isCandle = $$.isCandlestickType(v.id);
1,005✔
1155

1156
                        if (isBar || isCandle) {
1,005✔
1157
                                const selector = isBar ?
30✔
1158
                                        `.${$BAR.chartBar}.${$COMMON.target}${
1159
                                                $$.getTargetSelectorSuffix(v.id)
1160
                                        } .${$BAR.bar}-${v.index}` :
1161
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${
1162
                                                $$.getTargetSelectorSuffix(v.id)
1163
                                        } .${$CANDLESTICK.candlestick}-${v.index} path`;
1164

1165
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
30✔
1166
                                        closest = v;
6✔
1167
                                }
1168
                        } else {
1169
                                const d = $$.dist(v as IDataPoint, pos);
975✔
1170
                                const sensitivity = $$.getPointSensitivity(v);
975✔
1171

1172
                                if (d < sensitivity && (minDist === undefined || d < minDist)) {
975✔
1173
                                        minDist = d;
213✔
1174
                                        closest = v;
213✔
1175
                                }
1176
                        }
1177
                }
1178

1179
                return closest;
306✔
1180
        },
1181

1182
        dist(data: IDataPoint, pos: [number, number]) {
1183
                const $$ = this;
1,140✔
1184
                const {config: {axis_rotated: isRotated}, scale} = $$;
1,140✔
1185
                const xIndex = +isRotated; // true: 1, false: 0
1,140✔
1186
                const yIndex = +!isRotated; // true: 0, false: 1
1,140✔
1187
                const y = $$.circleY(data, data.index);
1,140✔
1188
                const x = (scale.zoom || scale.x)(data.x);
1,140✔
1189
                const dx = x - pos[xIndex];
1,140✔
1190
                const dy = y - pos[yIndex];
1,140✔
1191

1192
                return Math.sqrt(dx * dx + dy * dy);
1,140✔
1193
        },
1194

1195
        /**
1196
         * Convert data for step type
1197
         * @param {Array} values Object data values
1198
         * @returns {Array}
1199
         * @private
1200
         */
1201
        convertValuesToStep(values) {
1202
                const $$ = this;
702✔
1203
                const {axis, config} = $$;
702✔
1204
                const stepType = config.line_step_type;
702✔
1205
                const isCategorized = axis ? axis.isCategorized() : false;
702!
1206
                const converted = isArray(values) ? values.concat() : [values];
702!
1207

1208
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
702✔
1209
                        return values;
381✔
1210
                }
1211

1212
                // when all data are null, return empty array
1213
                // https://github.com/naver/billboard.js/issues/3124
1214
                if (converted.length) {
321!
1215
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
1216
                        const head = converted[0];
321✔
1217
                        const tail = converted[converted.length - 1];
321✔
1218
                        const {id} = head;
321✔
1219
                        let {x} = head;
321✔
1220

1221
                        // insert head
1222
                        converted.unshift({x: --x, value: head.value, id});
321✔
1223

1224
                        isCategorized && stepType === "step-after" &&
321✔
1225
                                converted.unshift({x: x - 1, value: head.value, id});
1226

1227
                        // append tail
1228
                        x = tail.x;
321✔
1229
                        converted.push({x: ++x, value: tail.value, id});
321✔
1230

1231
                        isCategorized && stepType === "step-before" &&
321✔
1232
                                converted.push({x: x + 1, value: tail.value, id});
1233
                }
1234

1235
                return converted;
321✔
1236
        },
1237

1238
        convertValuesToRange(values) {
1239
                const converted = isArray(values) ? values.concat() : [values];
3!
1240
                const ranges: {x: string | number, id: string, value: number}[] = [];
3✔
1241

1242
                converted.forEach(range => {
3✔
1243
                        const {x, id} = range;
3✔
1244

1245
                        ranges.push({
3✔
1246
                                x,
1247
                                id,
1248
                                value: range.value[0]
1249
                        });
1250

1251
                        ranges.push({
3✔
1252
                                x,
1253
                                id,
1254
                                value: range.value[2]
1255
                        });
1256
                });
1257

1258
                return ranges;
3✔
1259
        },
1260

1261
        updateDataAttributes(name, attrs) {
1262
                const $$ = this;
45✔
1263
                const {config} = $$;
45✔
1264
                const current = config[`data_${name}`];
45✔
1265

1266
                if (isUndefined(attrs)) {
45✔
1267
                        return current;
15✔
1268
                }
1269

1270
                Object.keys(attrs).forEach(id => {
30✔
1271
                        current[id] = attrs[id];
45✔
1272
                });
1273

1274
                $$.redraw({withLegend: true});
30✔
1275

1276
                return current;
30✔
1277
        },
1278

1279
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
6,039!
1280
                const value = d?.value;
8,169✔
1281

1282
                if (isArray(value)) {
8,169✔
1283
                        if (type === "bar") {
5,295✔
1284
                                return value.reduce((a, c) => c - a);
24✔
1285
                        } else {
1286
                                const index = rangedDataKeyIndex[type]?.[key as string] ?? -1;
5,271!
1287

1288
                                return index >= 0 ? value[index] : undefined;
5,271!
1289
                        }
1290
                } else if (value && key) {
2,874✔
1291
                        return value[key];
939✔
1292
                }
1293

1294
                return value;
1,935✔
1295
        },
1296

1297
        /**
1298
         * Set ratio for grouped data
1299
         * @param {Array} data Data array
1300
         * @private
1301
         */
1302
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
1303
                const $$ = this;
2,223✔
1304
                const {config} = $$;
2,223✔
1305

1306
                // calculate ratio if grouped data exists
1307
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
2,223✔
1308
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
6,702✔
1309

1310
                        data.forEach(v => {
507✔
1311
                                "values" in v ? v.values.forEach(setter) : setter(v);
6,093✔
1312
                        });
1313
                }
1314
        },
1315

1316
        /**
1317
         * Get ratio value
1318
         * @param {string} type Ratio for given type
1319
         * @param {object} d Data value object
1320
         * @param {boolean} asPercent Convert the return as percent or not
1321
         * @returns {number} Ratio value
1322
         * @private
1323
         */
1324
        getRatio(type: "arc" | "index" | "radar" | "bar" | "treemap", d, asPercent = false): number {
7,056✔
1325
                const $$ = this;
14,436✔
1326
                const {config, state} = $$;
14,436✔
1327
                const api = $$.api;
14,436✔
1328
                let ratio = 0;
14,436✔
1329

1330
                if (d && api.data.shown().length) {
14,436✔
1331
                        ratio = d.ratio || d.value;
14,391✔
1332

1333
                        if (type === "arc") {
14,391✔
1334
                                // if has padAngle set, calculate rate based on value
1335
                                if ($$.pie.padAngle()()) {
2,139✔
1336
                                        ratio = d.value / $$.getTotalDataSum(true);
66✔
1337

1338
                                        // otherwise, based on the rendered angle value
1339
                                } else {
1340
                                        const gaugeArcLength = config.gauge_fullCircle ?
2,073✔
1341
                                                $$.getArcLength() :
1342
                                                $$.getStartingAngle() * -2;
1343
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
2,073✔
1344

1345
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
2,073✔
1346
                                }
1347
                        } else if (type === "index") {
12,252✔
1348
                                const dataValues = api.data.values.bind(api);
7,380✔
1349
                                const {hiddenTargetIds} = state;
7,380✔
1350

1351
                                // For normalized stack per group, get total per group
1352
                                let total = this.getTotalPerIndex(
7,380✔
1353
                                        $$.isStackNormalizedPerGroup() ? d.id : undefined
7,380✔
1354
                                );
1355

1356
                                // If total is null, the data is not in any group - don't normalize
1357
                                if (total === null) {
7,380✔
1358
                                        return ratio;
54✔
1359
                                }
1360

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

1365
                                        if ($$.isStackNormalizedPerGroup() && d.id) {
567!
UNCOV
1366
                                                const group = config.data_groups.find(g => g.indexOf(d.id) >= 0);
×
UNCOV
1367
                                                if (group) {
×
1368
                                                        // Only consider hidden IDs in the same group
UNCOV
1369
                                                        hiddenIds = hiddenIds.filter(id => group.indexOf(id) >= 0);
×
1370
                                                }
1371
                                        }
1372

1373
                                        if (hiddenIds.length) {
567!
1374
                                                let hiddenSum = dataValues(hiddenIds, false);
567✔
1375

1376
                                                if (hiddenSum.length) {
567✔
1377
                                                        hiddenSum = hiddenSum
159✔
1378
                                                                .reduce((acc, curr) =>
1379
                                                                        acc.map((v, i) => (isNumber(v) ? v : 0) + curr[i])
144!
1380
                                                                );
1381

1382
                                                        total = total.map((v, i) => v - hiddenSum[i]);
387✔
1383
                                                }
1384
                                        }
1385
                                }
1386

1387
                                const divisor = total[d.index];
7,326✔
1388

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

1391
                                ratio = d.ratio;
7,326✔
1392
                        } else if (type === "radar") {
4,872✔
1393
                                ratio = (
1,542✔
1394
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
1395
                                ) * config.radar_size_ratio;
1396
                        } else if (type === "bar") {
3,330✔
1397
                                const yScale = $$.getYScaleById.bind($$)(d.id);
1,758✔
1398
                                const max = yScale.domain().reduce((a, c) => c - a);
1,758✔
1399

1400
                                // when all data are 0, return 0
1401
                                ratio = max === 0 ? 0 : Math.abs(
1,758✔
1402
                                        $$.getRangedData(d, null, type) / max
1403
                                );
1404
                        } else if (type === "treemap") {
1,572!
1405
                                ratio /= $$.getTotalDataSum(true);
1,572✔
1406
                        }
1407
                }
1408

1409
                return asPercent && ratio ? ratio * 100 : ratio;
14,382✔
1410
        },
1411

1412
        /**
1413
         * Sort data index to be aligned with x axis.
1414
         * @param {Array} tickValues Tick array values
1415
         * @private
1416
         */
1417
        updateDataIndexByX(tickValues) {
1418
                const $$ = this;
6,378✔
1419

1420
                const tickValueMap = tickValues.reduce((out, tick, index) => {
6,378✔
1421
                        out[Number(tick.x)] = index;
41,433✔
1422
                        return out;
41,433✔
1423
                }, {});
1424

1425
                $$.data.targets.forEach(t => {
6,378✔
1426
                        t.values.forEach((value, valueIndex) => {
12,918✔
1427
                                let index = tickValueMap[Number(value.x)];
69,795✔
1428

1429
                                if (index === undefined) {
69,795✔
1430
                                        index = valueIndex;
852✔
1431
                                }
1432
                                value.index = index;
69,795✔
1433
                        });
1434
                });
1435
        },
1436

1437
        /**
1438
         * Determine if bubble has dimension data
1439
         * @param {object|Array} d data value
1440
         * @returns {boolean}
1441
         * @private
1442
         */
1443
        isBubbleZType(d): boolean {
1444
                const $$ = this;
2,460,862✔
1445

1446
                return $$.isBubbleType(d) && (
2,460,862!
1447
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1448
                        (isArray(d.value) && d.value.length >= 2)
1449
                );
1450
        },
1451

1452
        /**
1453
         * Determine if bar has ranged data
1454
         * @param {Array} d data value
1455
         * @returns {boolean}
1456
         * @private
1457
         */
1458
        isBarRangeType(d): boolean {
1459
                const $$ = this;
88,551✔
1460
                const {value} = d;
88,551✔
1461

1462
                return $$.isBarType(d) &&
88,551✔
1463
                        !$$.isSubchartSourceTypeOf?.(d, TYPE.CANDLESTICK) &&
1464
                        isArray(value) && value.length >= 2 &&
1465
                        value.every(isNumber);
1466
        },
1467

1468
        /**
1469
         * Get data object by id
1470
         * @param {string} id data id
1471
         * @returns {object}
1472
         * @private
1473
         */
1474
        getDataById(id: string) {
1475
                const d = this.cache.get(id) || this.api.data(id);
25,599✔
1476

1477
                return d?.[0] ?? d;
25,599✔
1478
        }
1479
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc