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

naver / billboard.js / 21126911356

19 Jan 2026 05:55AM UTC coverage: 94.082% (-0.08%) from 94.157%
21126911356

push

github

web-flow
chore(deps-dev): update dependency (#4083)

update dependencies to the latest

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

6624 of 7321 branches covered (90.48%)

Branch coverage included in aggregate %.

8225 of 8462 relevant lines covered (97.2%)

25510.97 hits per line

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

92.13
/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;
29,763✔
35
                const {config} = $$;
29,763✔
36
                const dataKey = config.data_x && key === config.data_x;
29,763✔
37
                const existValue = notEmpty(config.data_xs) && hasValue(config.data_xs, key);
29,763✔
38

39
                return dataKey || existValue;
29,763✔
40
        },
41

42
        isNotX(key): boolean {
43
                return !this.isX(key);
14,130✔
44
        },
45

46
        isStackNormalized(): boolean {
47
                const {config} = this;
104,129✔
48

49
                return !!(
104,129✔
50
                        (config.data_stack_normalize === true ||
51
                                isObjectType(config.data_stack_normalize)) &&
52
                        config.data_groups.length
53
                );
54
        },
55

56
        /**
57
         * Check if stack normalization should be applied per group
58
         * @returns {boolean}
59
         * @private
60
         */
61
        isStackNormalizedPerGroup(): boolean {
62
                const {config} = this;
8,664✔
63

64
                return !!(
8,664✔
65
                        isObjectType(config.data_stack_normalize) &&
9,354✔
66
                        config.data_stack_normalize?.perGroup &&
67
                        config.data_groups.length
68
                );
69
        },
70

71
        /**
72
         * Check if given id is grouped data or has grouped data
73
         * @param {string} id Data id value
74
         * @returns {boolean} is grouped data or has grouped data
75
         * @private
76
         */
77
        isGrouped(id?: string): boolean {
78
                const groups = this.config.data_groups;
201,666✔
79

80
                return id ? groups.some(v => v.indexOf(id) >= 0 && v.length > 1) : groups.length > 0;
201,666!
81
        },
82

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

94
                // Get all data IDs that belong to this axis
95
                const axisDataIds = targets
234✔
96
                        .filter(t => axis.getId(t.id) === axisId)
594✔
97
                        .map(t => t.id);
594✔
98

99
                // Check if any of the axis data IDs are in groups
100
                return axisDataIds.some(id => $$.isGrouped(id));
234✔
101
        },
102

103
        getXKey(id) {
104
                const $$ = this;
28,266✔
105
                const {config} = $$;
28,266✔
106

107
                return config.data_x ?
28,266✔
108
                        config.data_x :
109
                        (notEmpty(config.data_xs) ? config.data_xs[id] : null);
20,814✔
110
        },
111

112
        getXValuesOfXKey(key, targets) {
113
                const $$ = this;
3✔
114
                const ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : [];
3!
115
                let xValues;
116

117
                ids.forEach(id => {
3✔
118
                        if ($$.getXKey(id) === key) {
6✔
119
                                xValues = $$.data.xs[id];
3✔
120
                        }
121
                });
122

123
                return xValues;
3✔
124
        },
125

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

136
                return basedX ?
285!
137
                        basedX.indexOf(isString(x) ? x : +x) :
×
138
                        ($$.filterByX($$.data.targets, x)[0] || {index: null}).index;
288✔
139
        },
140

141
        getXValue(id: string, i: number): number {
142
                const $$ = this;
192✔
143

144
                return id in $$.data.xs &&
192✔
145
                                $$.data.xs[id] &&
146
                                isValue($$.data.xs[id][i]) ?
147
                        $$.data.xs[id][i] :
148
                        i;
149
        },
150

151
        getOtherTargetXs(): string | null {
152
                const $$ = this;
33✔
153
                const idsForX = Object.keys($$.data.xs);
33✔
154

155
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
33!
156
        },
157

158
        getOtherTargetX(index: number): string | null {
159
                const xs = this.getOtherTargetXs();
27✔
160

161
                return xs && index < xs.length ? xs[index] : null;
27!
162
        },
163

164
        addXs(xs): void {
165
                const $$ = this;
6✔
166
                const {config} = $$;
6✔
167

168
                Object.keys(xs).forEach(id => {
6✔
169
                        config.data_xs[id] = xs[id];
6✔
170
                });
171
        },
172

173
        /**
174
         * Determine if x axis is multiple
175
         * @returns {boolean} true: multiple, false: single
176
         * @private
177
         */
178
        isMultipleX(): boolean {
179
                return !this.config.axis_x_forceAsSingle && (
89,733✔
180
                        notEmpty(this.config.data_xs) ||
181
                        this.hasType("bubble") ||
182
                        this.hasType("scatter")
183
                );
184
        },
185

186
        addName(data) {
187
                const $$ = this;
3,192✔
188
                const {config} = $$;
3,192✔
189
                let name;
190

191
                if (data) {
3,192✔
192
                        name = config.data_names[data.id];
3,168✔
193
                        data.name = name !== undefined ? name : data.id;
3,168!
194
                }
195

196
                return data;
3,192✔
197
        },
198

199
        /**
200
         * Get all values on given index
201
         * @param {number} index Index
202
         * @param {boolean} filterNull Filter nullish value
203
         * @returns {Array}
204
         * @private
205
         */
206
        getAllValuesOnIndex(index: number, filterNull = false) {
1,434✔
207
                const $$ = this;
1,494✔
208

209
                let value = $$.filterTargetsToShow($$.data.targets)
1,494✔
210
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
3,015✔
211

212
                if (filterNull) {
1,494✔
213
                        value = value.filter(v => v && "value" in v && isValue(v.value));
180✔
214
                }
215

216
                return value;
1,494✔
217
        },
218

219
        getValueOnIndex(values, index: number) {
220
                const valueOnIndex = values.filter(v => v.index === index);
15,132✔
221

222
                return valueOnIndex.length ? valueOnIndex[0] : null;
3,099✔
223
        },
224

225
        updateTargetX(targets, x) {
226
                const $$ = this;
9✔
227

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

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

237
        updateTargetXs(targets, xs) {
238
                const $$ = this;
3✔
239

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

245
        generateTargetX(rawX, id: string, index: number) {
246
                const $$ = this;
76,554✔
247
                const {axis} = $$;
76,554✔
248
                let x = axis?.isCategorized() ? index : (rawX || index);
76,554✔
249

250
                if (axis?.isTimeSeries()) {
76,554✔
251
                        const fn = parseDate.bind($$);
12,762✔
252

253
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
12,762✔
254
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
63,792✔
255
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
12,468✔
256
                }
257

258
                return x;
76,554✔
259
        },
260

261
        updateXs(values): void {
262
                if (values.length) {
6,146✔
263
                        this.axis.xs = values.map(v => v.x);
39,003✔
264
                }
265
        },
266

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

270
                return isDefined(x) ? x : null;
70,086✔
271
        },
272

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

276
                return isDefined(x) ? x : null;
70,086✔
277
        },
278

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

290
                // In case of area-range, data is given as: [low, mid, high] or {low, mid, high}
291
                // will take the 'mid' as the base value
292
                if (value && hasAxis) {
307,059✔
293
                        if ($$.isAreaRangeType(data)) {
285,720✔
294
                                value = $$.getRangedData(data, "mid");
3,141✔
295
                        } else if ($$.isBubbleZType(data)) {
282,579✔
296
                                value = $$.getBubbleZData(value, "y");
498✔
297
                        }
298
                }
299

300
                return value;
307,059✔
301
        },
302

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

314
                (data || this.data.targets.map(t => t.values))
402✔
315
                        .forEach((v, i) => {
316
                                const value = v.map(getBaseValue).filter(isNumber);
1,026✔
317

318
                                min = Math.min(i ? min : Infinity, ...value);
1,026✔
319
                                max = Math.max(i ? max : -Infinity, ...value);
1,026✔
320
                        });
321

322
                return {min, max};
402✔
323
        },
324

325
        /**
326
         * Get the min/max data
327
         * @private
328
         * @returns {{min: Array, max: Array}}
329
         */
330
        getMinMaxData(): {min: IDataRow[], max: IDataRow[]} {
331
                const $$ = this;
3,075✔
332
                const cacheKey = KEY.dataMinMax;
3,075✔
333
                let minMaxData = $$.cache.get(cacheKey);
3,075✔
334

335
                if (!minMaxData) {
3,075✔
336
                        const data = $$.data.targets.map(t => t.values);
1,008✔
337
                        const minMax = $$.getMinMaxValue(data);
396✔
338

339
                        let min = [];
396✔
340
                        let max = [];
396✔
341

342
                        // Cache the getFilteredDataByValue function calls
343
                        const {min: minVal, max: maxVal} = minMax;
396✔
344

345
                        data.forEach(v => {
396✔
346
                                const minData = $$.getFilteredDataByValue(v, minVal);
1,008✔
347
                                const maxData = $$.getFilteredDataByValue(v, maxVal);
1,008✔
348

349
                                if (minData.length) {
1,008✔
350
                                        min = min.concat(minData);
414✔
351
                                }
352

353
                                if (maxData.length) {
1,008✔
354
                                        max = max.concat(maxData);
396✔
355
                                }
356
                        });
357

358
                        // update the cached data
359
                        $$.cache.add(cacheKey, minMaxData = {min, max});
396✔
360
                }
361

362
                return minMaxData;
3,075✔
363
        },
364

365
        /**
366
         * Get sum of data per index
367
         * @param {string} targetId Target ID to get total for (only for normalized stack per group)
368
         * @private
369
         * @returns {Array}
370
         */
371
        getTotalPerIndex(targetId?: string) {
372
                const $$ = this;
7,524✔
373
                const {config} = $$;
7,524✔
374
                const cacheKey = targetId ? `${KEY.dataTotalPerIndex}-${targetId}` : KEY.dataTotalPerIndex;
7,524✔
375
                let sum = $$.cache.get(cacheKey);
7,524✔
376

377
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
7,524!
378
                        sum = [];
540✔
379

380
                        // When normalize per group is enabled and targetId is provided,
381
                        // only sum data within the same group
382
                        let {targets} = $$.data;
540✔
383

384
                        if ($$.isStackNormalizedPerGroup() && targetId) {
540✔
385
                                // Find which group the target belongs to
386
                                const group = config.data_groups.find(g => g.indexOf(targetId) >= 0);
96✔
387

388
                                if (group) {
84✔
389
                                        // Only sum targets in the same group
390
                                        targets = targets.filter(t => group.indexOf(t.id) >= 0);
126✔
391
                                } else {
392
                                        // If target is not in any group, return null to indicate no normalization
393
                                        return null;
54✔
394
                                }
395
                        }
396

397
                        targets.forEach(row => {
486✔
398
                                row.values.forEach((v, i) => {
2,619✔
399
                                        if (!sum[i]) {
7,371✔
400
                                                sum[i] = 0;
2,211✔
401
                                        }
402

403
                                        sum[i] += ~~v.value;
7,371✔
404
                                });
405
                        });
406

407
                        $$.cache.add(cacheKey, sum);
486✔
408
                }
409

410
                return sum;
7,470✔
411
        },
412

413
        /**
414
         * Get total data sum
415
         * @param {boolean} subtractHidden Subtract hidden data from total
416
         * @returns {number}
417
         * @private
418
         */
419
        getTotalDataSum(subtractHidden) {
420
                const $$ = this;
3,399✔
421
                const cacheKey = KEY.dataTotalSum;
3,399✔
422
                let total = $$.cache.get(cacheKey);
3,399✔
423

424
                if (!isNumber(total)) {
3,399✔
425
                        total = $$.data.targets.reduce((acc, t) => {
441✔
426
                                return acc + t.values.reduce((sum, v) => sum + (v.value ?? 0), 0);
1,665✔
427
                        }, 0);
428

429
                        $$.cache.add(cacheKey, total);
441✔
430
                }
431

432
                if (subtractHidden) {
3,399✔
433
                        total -= $$.getHiddenTotalDataSum();
2,397✔
434
                }
435

436
                return total;
3,399✔
437
        },
438

439
        /**
440
         * Get total hidden data sum
441
         * @returns {number}
442
         * @private
443
         */
444
        getHiddenTotalDataSum() {
445
                const $$ = this;
2,397✔
446
                const {api, state: {hiddenTargetIds}} = $$;
2,397✔
447
                let total = 0;
2,397✔
448

449
                if (hiddenTargetIds.length) {
2,397✔
450
                        total = api.data.values.bind(api)(hiddenTargetIds)
205✔
451
                                .reduce((p, c) => p + c);
12✔
452
                }
453

454
                return total;
2,397✔
455
        },
456

457
        /**
458
         * Get filtered data by value
459
         * @param {object} data Data
460
         * @param {number} value Value to be filtered
461
         * @returns {Array} filtered array data
462
         * @private
463
         */
464
        getFilteredDataByValue(data, value) {
465
                return data.filter(t => this.getBaseValue(t) === value);
5,118✔
466
        },
467

468
        /**
469
         * Return the max length of the data
470
         * @returns {number} max data length
471
         * @private
472
         */
473
        getMaxDataCount(): number {
474
                return Math.max(...this.data.targets.map(t => t.values.length), 0);
25,587✔
475
        },
476

477
        getMaxDataCountTarget() {
478
                let target = this.filterTargetsToShow() || [];
6,146!
479
                const length = target.length;
6,146✔
480
                const isInverted = this.config.axis_x_inverted;
6,146✔
481

482
                if (length > 1) {
6,146✔
483
                        target = target.map(t => t.values)
9,969✔
484
                                .reduce((a, b) => a.concat(b))
6,345✔
485
                                .map(v => v.x);
44,793✔
486

487
                        target = sortValue(getUnique(target))
3,624✔
488
                                .map((x, index, array) => ({
18,258✔
489
                                        x,
490
                                        index: isInverted ? array.length - index - 1 : index
18,258!
491
                                }));
492
                } else if (length) {
2,522✔
493
                        target = target[0].values.concat();
2,348✔
494
                }
495

496
                return target;
6,146✔
497
        },
498

499
        mapToIds(targets): string[] {
500
                return targets.map(d => d.id);
165,191✔
501
        },
502

503
        mapToTargetIds(ids?: string[] | string): string[] {
504
                const $$ = this;
6,168✔
505

506
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
6,168✔
507
        },
508

509
        hasTarget(targets, id): boolean {
510
                const ids = this.mapToIds(targets);
87✔
511

512
                for (let i = 0, val; (val = ids[i]); i++) {
87✔
513
                        if (val === id) {
177✔
514
                                return true;
87✔
515
                        }
516
                }
517

518
                return false;
×
519
        },
520

521
        isTargetToShow(targetId): boolean {
522
                return this.state.hiddenTargetIds.indexOf(targetId) < 0;
355,594✔
523
        },
524

525
        isLegendToShow(targetId): boolean {
526
                return this.state.hiddenLegendIds.indexOf(targetId) < 0;
36,441✔
527
        },
528

529
        filterTargetsToShow(targets?) {
530
                const $$ = this;
130,386✔
531

532
                // When called without arguments, use caching
533
                if (!targets) {
130,386✔
534
                        const {cache, data, state} = $$;
31,958✔
535
                        const cacheKey = KEY.filteredTargets;
31,958✔
536
                        const visibilityChecksum = state.hiddenTargetIds.join(",");
31,958✔
537
                        const storedChecksum = cache.get(KEY.visibilityChecksum);
31,958✔
538

539
                        // Invalidate cache if visibility changed
540
                        if (visibilityChecksum !== storedChecksum) {
31,958✔
541
                                cache.remove(cacheKey);
30,622✔
542
                                cache.add(KEY.visibilityChecksum, visibilityChecksum);
30,622✔
543
                        }
544

545
                        // Return cached result if available
546
                        if (cache.has(cacheKey)) {
31,958✔
547
                                return cache.get(cacheKey);
1,336✔
548
                        }
549

550
                        // Compute and cache result (store the filtered array)
551
                        const filtered = data.targets.filter(t => $$.isTargetToShow(t.id));
68,271✔
552
                        cache.add(cacheKey, filtered);
30,622✔
553

554
                        return filtered;
30,622✔
555
                }
556

557
                // When called with custom targets, don't cache
558
                return targets.filter(t => $$.isTargetToShow(t.id));
255,682✔
559
        },
560

561
        mapTargetsToUniqueXs(targets) {
562
                const $$ = this;
23,337✔
563
                const {axis} = $$;
23,337✔
564
                let xs: any[] = [];
23,337✔
565

566
                if (targets?.length) {
23,337✔
567
                        xs = getUnique(
23,109✔
568
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
261,351✔
569
                        );
570

571
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
55,320✔
572
                }
573

574
                return sortValue(xs);
23,337✔
575
        },
576

577
        /**
578
         * Add to thetarget Ids
579
         * @param {string} type State's prop name
580
         * @param {Array|string} targetIds Target ids array
581
         * @private
582
         */
583
        addTargetIds(type: string, targetIds: string[] | string): void {
584
                const {state} = this;
264✔
585
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as [];
264✔
586

587
                ids.forEach(v => {
264✔
588
                        state[type].indexOf(v) < 0 &&
363✔
589
                                state[type].push(v);
590
                });
591
        },
592

593
        /**
594
         * Remove from the state target Ids
595
         * @param {string} type State's prop name
596
         * @param {Array|string} targetIds Target ids array
597
         * @private
598
         */
599
        removeTargetIds(type: string, targetIds: string[] | string): void {
600
                const {state} = this;
201✔
601
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as [];
201!
602

603
                ids.forEach(v => {
201✔
604
                        const index = state[type].indexOf(v);
414✔
605

606
                        index >= 0 && state[type].splice(index, 1);
414✔
607
                });
608
        },
609

610
        addHiddenTargetIds(targetIds: string[]): void {
611
                this.addTargetIds("hiddenTargetIds", targetIds);
243✔
612
        },
613

614
        removeHiddenTargetIds(targetIds: string[]): void {
615
                this.removeTargetIds("hiddenTargetIds", targetIds);
87✔
616
        },
617

618
        addHiddenLegendIds(targetIds: string[]): void {
619
                this.addTargetIds("hiddenLegendIds", targetIds);
21✔
620
        },
621

622
        removeHiddenLegendIds(targetIds: string[]): void {
623
                this.removeTargetIds("hiddenLegendIds", targetIds);
114✔
624
        },
625

626
        getValuesAsIdKeyed(targets) {
627
                const $$ = this;
63,402✔
628
                const {hasAxis} = $$.state;
63,402✔
629
                const ys = {};
63,402✔
630
                const isMultipleX = $$.isMultipleX();
63,402✔
631
                const xs = isMultipleX ?
63,402✔
632
                        $$.mapTargetsToUniqueXs(targets)
633
                                .map(v => (isString(v) ? v : +v)) :
14,328!
634
                        null;
635

636
                // Create xIndexMap for O(1) lookup instead of O(n) indexOf in getIndexByX
637
                const xIndexMap = xs ? new Map(xs.map((x, i) => [x, i])) : null;
63,402✔
638

639
                targets.forEach(t => {
63,402✔
640
                        const data: any[] = [];
121,872✔
641

642
                        t.values
121,872✔
643
                                .filter(({value}) => isValue(value) || value === null)
698,052✔
644
                                .forEach(v => {
645
                                        let {value} = v;
698,052✔
646

647
                                        // exclude 'volume' value to correct mis domain calculation
648
                                        if (value !== null && $$.isCandlestickType(v)) {
698,052✔
649
                                                value = isArray(value) ?
1,092✔
650
                                                        value.slice(0, 4) :
651
                                                        [value.open, value.high, value.low, value.close];
652
                                        }
653

654
                                        if (isArray(value)) {
698,052✔
655
                                                data.push(...value);
6,504✔
656
                                        } else if (isObject(value) && "high" in value) {
691,548✔
657
                                                data.push(...Object.values(value));
720✔
658
                                        } else if ($$.isBubbleZType(v)) {
690,828✔
659
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
540✔
660
                                        } else {
661
                                                if (isMultipleX && xIndexMap) {
690,288✔
662
                                                        // Use Map for O(1) lookup instead of getIndexByX which uses indexOf
663
                                                        const xKey = isString(v.x) ? v.x : +v.x;
21,402!
664
                                                        const index = xIndexMap.get(xKey);
21,402✔
665

666
                                                        if (index !== undefined) {
21,402!
667
                                                                data[index as number] = value;
21,402✔
668
                                                        }
669
                                                } else {
670
                                                        data.push(value);
668,886✔
671
                                                }
672
                                        }
673
                                });
674

675
                        ys[t.id] = data;
121,872✔
676
                });
677

678
                return ys;
63,402✔
679
        },
680

681
        checkValueInTargets(targets, checker: Function): boolean {
682
                return Object.keys(targets)
18,744✔
683
                        .some(id => targets[id].values.some(v => checker(v.value)));
100,443✔
684
        },
685

686
        hasMultiTargets(): boolean {
687
                return this.filterTargetsToShow().length > 1;
264✔
688
        },
689

690
        hasNegativeValueInTargets(targets): boolean {
691
                return this.checkValueInTargets(targets, v => v < 0);
87,168✔
692
        },
693

694
        hasPositiveValueInTargets(targets): boolean {
695
                return this.checkValueInTargets(targets, v => v > 0);
13,275✔
696
        },
697

698
        /**
699
         * Sort targets data
700
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
701
         * @param {Array} targetsValue Target value
702
         * @returns {Array}
703
         * @private
704
         */
705
        orderTargets(targetsValue: IData[]): IData[] {
706
                const $$ = this;
23,491✔
707
                const targets = [...targetsValue];
23,491✔
708
                const fn = $$.getSortCompareFn();
23,491✔
709

710
                fn && targets.sort(fn);
23,491✔
711

712
                return targets;
23,491✔
713
        },
714

715
        /**
716
         * Get data.order compare function
717
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
718
         * @returns {function} compare function
719
         * @private
720
         */
721
        getSortCompareFn(isReversed = false): Function | null {
23,491✔
722
                const $$ = this;
24,361✔
723
                const {config} = $$;
24,361✔
724
                const order = config.data_order;
24,361✔
725
                const orderAsc = /asc/i.test(order);
24,361✔
726
                const orderDesc = /desc/i.test(order);
24,361✔
727
                let fn;
728

729
                if (orderAsc || orderDesc) {
24,361✔
730
                        const reducer = (p, c) => p + Math.abs(c.value);
308,084✔
731
                        const sum = v => (isNumber(v) ? v : (
122,264✔
732
                                "values" in v ? v.values.reduce(reducer, 0) : v.value
121,214✔
733
                        ));
734

735
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
23,689✔
736
                                const t1Sum = sum(t1);
61,132✔
737
                                const t2Sum = sum(t2);
61,132✔
738

739
                                return isReversed ?
61,132✔
740
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
30,598!
741
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
30,534✔
742
                        };
743
                } else if (isFunction(order)) {
672✔
744
                        fn = order.bind($$.api);
54✔
745
                }
746

747
                return fn || null;
24,361✔
748
        },
749

750
        filterByX(targets, x) {
751
                return mergeArray(targets.map(t => t.values)).filter(v => v.x - x === 0);
3,108✔
752
        },
753

754
        filterNullish(data) {
755
                const filter = v => isValue(v.value);
72,687✔
756

757
                return data ?
20,591!
758
                        data.filter(
759
                                v => "value" in v ? filter(v) : v.values.some(filter)
72,450✔
760
                        ) :
761
                        data;
762
        },
763

764
        filterRemoveNull(data) {
765
                return data.filter(d => isValue(this.getBaseValue(d)));
276✔
766
        },
767

768
        filterByXDomain(targets, xDomain) {
769
                return targets.map(t => ({
153✔
770
                        id: t.id,
771
                        id_org: t.id_org,
772
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,842✔
773
                }));
774
        },
775

776
        hasDataLabel() {
777
                const dataLabels = this.config.data_labels;
70,991✔
778

779
                return (isBoolean(dataLabels) && dataLabels) ||
70,991✔
780
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
781
        },
782

783
        /**
784
         * Determine if has null value
785
         * @param {Array} targets Data array to be evaluated
786
         * @returns {boolean}
787
         * @private
788
         */
789
        hasNullDataValue(targets: IDataRow[]): boolean {
790
                return targets.some(({value}) => value === null);
312✔
791
        },
792

793
        /**
794
         * Get data index from the event coodinates
795
         * @param {Event} event Event object
796
         * @returns {number}
797
         * @private
798
         */
799
        getDataIndexFromEvent(event): number {
800
                const $$ = this;
1,551✔
801
                const {
802
                        $el,
803
                        config,
804
                        state: {hasRadar, inputType, eventReceiver: {coords, rect}}
805
                } = $$;
1,551✔
806
                let index;
807

808
                if (hasRadar) {
1,551✔
809
                        let target = event.target;
33✔
810

811
                        // in case of multilined axis text
812
                        if (/tspan/i.test(target.tagName)) {
33!
813
                                target = target.parentNode;
×
814
                        }
815

816
                        const d: any = d3Select(target).datum();
33✔
817

818
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
33!
819
                } else {
820
                        const isRotated = config.axis_rotated;
1,518✔
821
                        const scrollPos = getScrollPosition($el.chart.node());
1,518✔
822

823
                        // get data based on the mouse coords
824
                        const e = inputType === "touch" && event.changedTouches ?
1,518✔
825
                                event.changedTouches[0] :
826
                                event;
827

828
                        let point = isRotated ? e.clientY + scrollPos.y : e.clientX + scrollPos.x;
1,518✔
829

830
                        if (hasViewBox($el.svg)) {
1,518✔
831
                                const pos = [point, 0];
39✔
832

833
                                isRotated && pos.reverse();
39✔
834
                                point = getTransformCTM($el.eventRect.node(), ...pos)[isRotated ? "y" : "x"];
39✔
835
                        } else {
836
                                point -= isRotated ? rect.top : rect.left;
1,479✔
837
                        }
838

839
                        index = findIndex(
1,518✔
840
                                coords,
841
                                point,
842
                                0,
843
                                coords.length - 1,
844
                                isRotated
845
                        );
846
                }
847

848
                return index;
1,551✔
849
        },
850

851
        getDataLabelLength(min: number, max: number, key: "width" | "height"): number[] {
852
                const $$ = this;
3,141✔
853
                const paddingCoef = 1.3;
3,141✔
854

855
                return $$.getTextRect(
3,141✔
856
                        [min, max].map(v => $$.dataLabelFormat()(v))
6,282✔
857
                )?.map((rect: DOMRect) => rect[key] * paddingCoef) || [0, 0];
5,004✔
858
        },
859

860
        isNoneArc(d) {
861
                return this.hasTarget(this.data.targets, d.id);
×
862
        },
863

864
        isArc(d) {
865
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
866
        },
867

868
        findSameXOfValues(values, index) {
869
                const targetX = values[index].x;
×
870
                const sames: any[] = [];
×
871
                let i;
872

873
                for (i = index - 1; i >= 0; i--) {
×
874
                        if (targetX !== values[i].x) {
×
875
                                break;
×
876
                        }
877

878
                        sames.push(values[i]);
×
879
                }
880

881
                for (i = index; i < values.length; i++) {
×
882
                        if (targetX !== values[i].x) {
×
883
                                break;
×
884
                        }
885

886
                        sames.push(values[i]);
×
887
                }
888

889
                return sames;
×
890
        },
891

892
        findClosestFromTargets(targets, pos: [number, number]): IDataRow | undefined {
893
                const $$ = this;
117✔
894
                const candidates = targets.map(target => $$.findClosest(target.values, pos)); // map to array of closest points of each target
207✔
895

896
                // decide closest point and return
897
                return $$.findClosest(candidates, pos);
117✔
898
        },
899

900
        findClosest(values, pos: [number, number]): IDataRow | undefined {
901
                const $$ = this;
306✔
902
                const {$el: {main}} = $$;
306✔
903
                const data = values.filter(v => v && isValue(v.value));
1,104✔
904

905
                let minDist;
906
                let closest;
907

908
                // find mouseovering bar/candlestick
909
                // https://github.com/naver/billboard.js/issues/2434
910
                data
306✔
911
                        .filter(v => $$.isBarType(v.id) || $$.isCandlestickType(v.id))
1,005✔
912
                        .forEach(v => {
913
                                const selector = $$.isBarType(v.id) ?
30✔
914
                                        `.${$BAR.chartBar}.${$COMMON.target}${
915
                                                $$.getTargetSelectorSuffix(v.id)
916
                                        } .${$BAR.bar}-${v.index}` :
917
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${
918
                                                $$.getTargetSelectorSuffix(v.id)
919
                                        } .${$CANDLESTICK.candlestick}-${v.index} path`;
920

921
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
30✔
922
                                        closest = v;
6✔
923
                                }
924
                        });
925

926
                // find closest point from non-bar/candlestick
927
                data
306✔
928
                        .filter(v => !$$.isBarType(v.id) && !$$.isCandlestickType(v.id))
1,005✔
929
                        .forEach((v: IDataPoint) => {
930
                                const d = $$.dist(v, pos);
975✔
931

932
                                minDist = $$.getPointSensitivity(v);
975✔
933

934
                                if (d < minDist) {
975✔
935
                                        minDist = d;
216✔
936
                                        closest = v;
216✔
937
                                }
938
                        });
939

940
                return closest;
306✔
941
        },
942

943
        dist(data: IDataPoint, pos: [number, number]) {
944
                const $$ = this;
1,101✔
945
                const {config: {axis_rotated: isRotated}, scale} = $$;
1,101✔
946
                const xIndex = +isRotated; // true: 1, false: 0
1,101✔
947
                const yIndex = +!isRotated; // true: 0, false: 1
1,101✔
948
                const y = $$.circleY(data, data.index);
1,101✔
949
                const x = (scale.zoom || scale.x)(data.x);
1,101✔
950

951
                return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2));
1,101✔
952
        },
953

954
        /**
955
         * Convert data for step type
956
         * @param {Array} values Object data values
957
         * @returns {Array}
958
         * @private
959
         */
960
        convertValuesToStep(values) {
961
                const $$ = this;
570✔
962
                const {axis, config} = $$;
570✔
963
                const stepType = config.line_step_type;
570✔
964
                const isCategorized = axis ? axis.isCategorized() : false;
570!
965
                const converted = isArray(values) ? values.concat() : [values];
570!
966

967
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
570✔
968
                        return values;
279✔
969
                }
970

971
                // when all datas are null, return empty array
972
                // https://github.com/naver/billboard.js/issues/3124
973
                if (converted.length) {
291!
974
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
975
                        const head = converted[0];
291✔
976
                        const tail = converted[converted.length - 1];
291✔
977
                        const {id} = head;
291✔
978
                        let {x} = head;
291✔
979

980
                        // insert head
981
                        converted.unshift({x: --x, value: head.value, id});
291✔
982

983
                        isCategorized && stepType === "step-after" &&
291✔
984
                                converted.unshift({x: --x, value: head.value, id});
985

986
                        // append tail
987
                        x = tail.x;
291✔
988
                        converted.push({x: ++x, value: tail.value, id});
291✔
989

990
                        isCategorized && stepType === "step-before" &&
291✔
991
                                converted.push({x: ++x, value: tail.value, id});
992
                }
993

994
                return converted;
291✔
995
        },
996

997
        convertValuesToRange(values) {
998
                const converted = isArray(values) ? values.concat() : [values];
×
999
                const ranges: {x: string | number, id: string, value: number}[] = [];
×
1000

1001
                converted.forEach(range => {
×
1002
                        const {x, id} = range;
×
1003

1004
                        ranges.push({
×
1005
                                x,
1006
                                id,
1007
                                value: range.value[0]
1008
                        });
1009

1010
                        ranges.push({
×
1011
                                x,
1012
                                id,
1013
                                value: range.value[2]
1014
                        });
1015
                });
1016

1017
                return ranges;
×
1018
        },
1019

1020
        updateDataAttributes(name, attrs) {
1021
                const $$ = this;
36✔
1022
                const {config} = $$;
36✔
1023
                const current = config[`data_${name}`];
36✔
1024

1025
                if (isUndefined(attrs)) {
36✔
1026
                        return current;
12✔
1027
                }
1028

1029
                Object.keys(attrs).forEach(id => {
24✔
1030
                        current[id] = attrs[id];
39✔
1031
                });
1032

1033
                $$.redraw({withLegend: true});
24✔
1034

1035
                return current;
24✔
1036
        },
1037

1038
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
4,095!
1039
                const value = d?.value;
6,198✔
1040

1041
                if (isArray(value)) {
6,198✔
1042
                        if (type === "bar") {
3,450✔
1043
                                return value.reduce((a, c) => c - a);
24✔
1044
                        } else {
1045
                                // @ts-ignore
1046
                                const index = {
3,426✔
1047
                                        areaRange: ["high", "mid", "low"],
1048
                                        candlestick: ["open", "high", "low", "close", "volume"]
1049
                                }[type].indexOf(key);
1050

1051
                                return index >= 0 && value ? value[index] : undefined;
3,426!
1052
                        }
1053
                } else if (value && key) {
2,748✔
1054
                        return value[key];
813✔
1055
                }
1056

1057
                return value;
1,935✔
1058
        },
1059

1060
        /**
1061
         * Set ratio for grouped data
1062
         * @param {Array} data Data array
1063
         * @private
1064
         */
1065
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
1066
                const $$ = this;
2,250✔
1067
                const {config} = $$;
2,250✔
1068

1069
                // calculate ratio if grouped data exists
1070
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
2,250✔
1071
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
6,846✔
1072

1073
                        data.forEach(v => {
519✔
1074
                                "values" in v ? v.values.forEach(setter) : setter(v);
6,237✔
1075
                        });
1076
                }
1077
        },
1078

1079
        /**
1080
         * Get ratio value
1081
         * @param {string} type Ratio for given type
1082
         * @param {object} d Data value object
1083
         * @param {boolean} asPercent Convert the return as percent or not
1084
         * @returns {number} Ratio value
1085
         * @private
1086
         */
1087
        getRatio(type: "arc" | "index" | "radar" | "bar" | "treemap", d, asPercent = false): number {
6,897✔
1088
                const $$ = this;
14,421✔
1089
                const {config, state} = $$;
14,421✔
1090
                const api = $$.api;
14,421✔
1091
                let ratio = 0;
14,421✔
1092

1093
                if (d && api.data.shown().length) {
14,421✔
1094
                        ratio = d.ratio || d.value;
14,373✔
1095

1096
                        if (type === "arc") {
14,373✔
1097
                                // if has padAngle set, calculate rate based on value
1098
                                if ($$.pie.padAngle()()) {
2,145✔
1099
                                        ratio = d.value / $$.getTotalDataSum(true);
66✔
1100

1101
                                        // otherwise, based on the rendered angle value
1102
                                } else {
1103
                                        const gaugeArcLength = config.gauge_fullCircle ?
2,079✔
1104
                                                $$.getArcLength() :
1105
                                                $$.getStartingAngle() * -2;
1106
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
2,079✔
1107

1108
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
2,079✔
1109
                                }
1110
                        } else if (type === "index") {
12,228✔
1111
                                const dataValues = api.data.values.bind(api);
7,524✔
1112
                                const {hiddenTargetIds} = state;
7,524✔
1113

1114
                                // For normalized stack per group, get total per group
1115
                                let total = this.getTotalPerIndex(
7,524✔
1116
                                        $$.isStackNormalizedPerGroup() ? d.id : undefined
7,524✔
1117
                                );
1118

1119
                                // If total is null, the data is not in any group - don't normalize
1120
                                if (total === null) {
7,524✔
1121
                                        return ratio;
54✔
1122
                                }
1123

1124
                                if (hiddenTargetIds.length) {
7,470✔
1125
                                        // When normalized per group, only subtract hidden data from the same group
1126
                                        let hiddenIds = hiddenTargetIds;
567✔
1127

1128
                                        if ($$.isStackNormalizedPerGroup() && d.id) {
567!
1129
                                                const group = config.data_groups.find(g => g.indexOf(d.id) >= 0);
×
1130
                                                if (group) {
×
1131
                                                        // Only consider hidden IDs in the same group
1132
                                                        hiddenIds = hiddenIds.filter(id => group.indexOf(id) >= 0);
×
1133
                                                }
1134
                                        }
1135

1136
                                        if (hiddenIds.length) {
567!
1137
                                                let hiddenSum = dataValues(hiddenIds, false);
567✔
1138

1139
                                                if (hiddenSum.length) {
567✔
1140
                                                        hiddenSum = hiddenSum
159✔
1141
                                                                .reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));
144✔
1142

1143
                                                        total = total.map((v, i) => v - hiddenSum[i]);
387✔
1144
                                                }
1145
                                        }
1146
                                }
1147

1148
                                const divisor = total[d.index];
7,470✔
1149

1150
                                d.ratio = isNumber(d.value) && total && divisor ? d.value / divisor : 0;
7,470✔
1151

1152
                                ratio = d.ratio;
7,470✔
1153
                        } else if (type === "radar") {
4,704✔
1154
                                ratio = (
1,542✔
1155
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
1156
                                ) * config.radar_size_ratio;
1157
                        } else if (type === "bar") {
3,162✔
1158
                                const yScale = $$.getYScaleById.bind($$)(d.id);
1,758✔
1159
                                const max = yScale.domain().reduce((a, c) => c - a);
1,758✔
1160

1161
                                // when all data are 0, return 0
1162
                                ratio = max === 0 ? 0 : Math.abs(
1,758✔
1163
                                        $$.getRangedData(d, null, type) / max
1164
                                );
1165
                        } else if (type === "treemap") {
1,404!
1166
                                ratio /= $$.getTotalDataSum(true);
1,404✔
1167
                        }
1168
                }
1169

1170
                return asPercent && ratio ? ratio * 100 : ratio;
14,367✔
1171
        },
1172

1173
        /**
1174
         * Sort data index to be aligned with x axis.
1175
         * @param {Array} tickValues Tick array values
1176
         * @private
1177
         */
1178
        updateDataIndexByX(tickValues) {
1179
                const $$ = this;
6,146✔
1180

1181
                const tickValueMap = tickValues.reduce((out, tick, index) => {
6,146✔
1182
                        out[Number(tick.x)] = index;
39,003✔
1183
                        return out;
39,003✔
1184
                }, {});
1185

1186
                $$.data.targets.forEach(t => {
6,146✔
1187
                        t.values.forEach((value, valueIndex) => {
12,557✔
1188
                                let index = tickValueMap[Number(value.x)];
66,852✔
1189

1190
                                if (index === undefined) {
66,852✔
1191
                                        index = valueIndex;
870✔
1192
                                }
1193
                                value.index = index;
66,852✔
1194
                        });
1195
                });
1196
        },
1197

1198
        /**
1199
         * Determine if bubble has dimension data
1200
         * @param {object|Array} d data value
1201
         * @returns {boolean}
1202
         * @private
1203
         */
1204
        isBubbleZType(d): boolean {
1205
                const $$ = this;
1,015,341✔
1206

1207
                return $$.isBubbleType(d) && (
1,015,341!
1208
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1209
                        (isArray(d.value) && d.value.length >= 2)
1210
                );
1211
        },
1212

1213
        /**
1214
         * Determine if bar has ranged data
1215
         * @param {Array} d data value
1216
         * @returns {boolean}
1217
         * @private
1218
         */
1219
        isBarRangeType(d): boolean {
1220
                const $$ = this;
77,907✔
1221
                const {value} = d;
77,907✔
1222

1223
                return $$.isBarType(d) && isArray(value) && value.length >= 2 &&
77,907✔
1224
                        value.every(isNumber);
1225
        },
1226

1227
        /**
1228
         * Get data object by id
1229
         * @param {string} id data id
1230
         * @returns {object}
1231
         * @private
1232
         */
1233
        getDataById(id: string) {
1234
                const d = this.cache.get(id) || this.api.data(id);
24,594✔
1235

1236
                return d?.[0] ?? d;
24,594✔
1237
        }
1238
};
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