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

naver / billboard.js / 16137267914

08 Jul 2025 07:51AM UTC coverage: 86.941% (-7.2%) from 94.118%
16137267914

push

github

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

update dependencies to the latest versions

5668 of 7016 branches covered (80.79%)

Branch coverage included in aggregate %.

7487 of 8115 relevant lines covered (92.26%)

11761.7 hits per line

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

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

39
                return dataKey || existValue;
14,190✔
40
        },
41

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

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

49
                return !!(config.data_stack_normalize && config.data_groups.length);
53,403!
50
        },
51

52
        /**
53
         * Check if given id is grouped data or has grouped data
54
         * @param {string} id Data id value
55
         * @returns {boolean} is grouped data or has grouped data
56
         * @private
57
         */
58
        isGrouped(id?: string): boolean {
59
                const groups = this.config.data_groups;
85,056✔
60

61
                return id ? groups.some(v => v.indexOf(id) >= 0 && v.length > 1) : groups.length > 0;
85,056!
62
        },
63

64
        getXKey(id) {
65
                const $$ = this;
13,434✔
66
                const {config} = $$;
13,434✔
67

68
                return config.data_x ?
13,434✔
69
                        config.data_x :
70
                        (notEmpty(config.data_xs) ? config.data_xs[id] : null);
10,710✔
71
        },
72

73
        getXValuesOfXKey(key, targets) {
74
                const $$ = this;
3✔
75
                const ids = targets && notEmpty(targets) ? $$.mapToIds(targets) : [];
3!
76
                let xValues;
77

78
                ids.forEach(id => {
3✔
79
                        if ($$.getXKey(id) === key) {
6✔
80
                                xValues = $$.data.xs[id];
3✔
81
                        }
82
                });
83

84
                return xValues;
3✔
85
        },
86

87
        /**
88
         * Get index number based on given x Axis value
89
         * @param {Date|number|string} x x Axis to be compared
90
         * @param {Array} basedX x Axis list to be based on
91
         * @returns {number} index number
92
         * @private
93
         */
94
        getIndexByX(x: Date | number | string, basedX: (Date | number | string)[]): number {
95
                const $$ = this;
12,600✔
96

97
                return basedX ?
12,600✔
98
                        basedX.indexOf(isString(x) ? x : +x) :
12,438!
99
                        ($$.filterByX($$.data.targets, x)[0] || {index: null}).index;
165✔
100
        },
101

102
        getXValue(id: string, i: number): number {
103
                const $$ = this;
126✔
104

105
                return id in $$.data.xs &&
126✔
106
                                $$.data.xs[id] &&
107
                                isValue($$.data.xs[id][i]) ?
108
                        $$.data.xs[id][i] :
109
                        i;
110
        },
111

112
        getOtherTargetXs(): string | null {
113
                const $$ = this;
30✔
114
                const idsForX = Object.keys($$.data.xs);
30✔
115

116
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
30!
117
        },
118

119
        getOtherTargetX(index: number): string | null {
120
                const xs = this.getOtherTargetXs();
27✔
121

122
                return xs && index < xs.length ? xs[index] : null;
27!
123
        },
124

125
        addXs(xs): void {
126
                const $$ = this;
6✔
127
                const {config} = $$;
6✔
128

129
                Object.keys(xs).forEach(id => {
6✔
130
                        config.data_xs[id] = xs[id];
6✔
131
                });
132
        },
133

134
        /**
135
         * Determine if x axis is multiple
136
         * @returns {boolean} true: multiple, false: single
137
         * @private
138
         */
139
        isMultipleX(): boolean {
140
                return !this.config.axis_x_forceAsSingle && (
40,665✔
141
                        notEmpty(this.config.data_xs) ||
142
                        this.hasType("bubble") ||
143
                        this.hasType("scatter")
144
                );
145
        },
146

147
        addName(data) {
148
                const $$ = this;
1,023✔
149
                const {config} = $$;
1,023✔
150
                let name;
151

152
                if (data) {
1,023✔
153
                        name = config.data_names[data.id];
1,008✔
154
                        data.name = name !== undefined ? name : data.id;
1,008!
155
                }
156

157
                return data;
1,023✔
158
        },
159

160
        /**
161
         * Get all values on given index
162
         * @param {number} index Index
163
         * @param {boolean} filterNull Filter nullish value
164
         * @returns {Array}
165
         * @private
166
         */
167
        getAllValuesOnIndex(index: number, filterNull = false) {
384✔
168
                const $$ = this;
390✔
169

170
                let value = $$.filterTargetsToShow($$.data.targets)
390✔
171
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
894✔
172

173
                if (filterNull) {
390✔
174
                        value = value.filter(v => v && "value" in v && isValue(v.value));
18✔
175
                }
176

177
                return value;
390✔
178
        },
179

180
        getValueOnIndex(values, index: number) {
181
                const valueOnIndex = values.filter(v => v.index === index);
4,710✔
182

183
                return valueOnIndex.length ? valueOnIndex[0] : null;
972✔
184
        },
185

186
        updateTargetX(targets, x) {
187
                const $$ = this;
9✔
188

189
                targets.forEach(t => {
9✔
190
                        t.values.forEach((v, i) => {
12✔
191
                                v.x = $$.generateTargetX(x[i], t.id, i);
36✔
192
                        });
193

194
                        $$.data.xs[t.id] = x;
12✔
195
                });
196
        },
197

198
        updateTargetXs(targets, xs) {
199
                const $$ = this;
3✔
200

201
                targets.forEach(t => {
3✔
202
                        xs[t.id] && $$.updateTargetX([t], xs[t.id]);
6✔
203
                });
204
        },
205

206
        generateTargetX(rawX, id: string, index: number) {
207
                const $$ = this;
32,601✔
208
                const {axis} = $$;
32,601✔
209
                let x = axis?.isCategorized() ? index : (rawX || index);
32,601✔
210

211
                if (axis?.isTimeSeries()) {
32,601✔
212
                        const fn = parseDate.bind($$);
3,216✔
213

214
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
3,216✔
215
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
29,385✔
216
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
9,522✔
217
                }
218

219
                return x;
32,601✔
220
        },
221

222
        updateXs(values): void {
223
                if (values.length) {
2,955✔
224
                        this.axis.xs = values.map(v => v.x);
16,806✔
225
                }
226
        },
227

228
        getPrevX(i: number): number[] | null {
229
                const x = this.axis.xs[i - 1];
29,364✔
230

231
                return isDefined(x) ? x : null;
29,364✔
232
        },
233

234
        getNextX(i: number): number[] | null {
235
                const x = this.axis.xs[i + 1];
29,364✔
236

237
                return isDefined(x) ? x : null;
29,364✔
238
        },
239

240
        /**
241
         * Get base value isAreaRangeType
242
         * @param {object} data Data object
243
         * @returns {number}
244
         * @private
245
         */
246
        getBaseValue(data): number {
247
                const $$ = this;
155,508✔
248
                const {hasAxis} = $$.state;
155,508✔
249
                let {value} = data;
155,508✔
250

251
                // In case of area-range, data is given as: [low, mid, high] or {low, mid, high}
252
                // will take the 'mid' as the base value
253
                if (value && hasAxis) {
155,508✔
254
                        if ($$.isAreaRangeType(data)) {
141,858✔
255
                                value = $$.getRangedData(data, "mid");
1,026✔
256
                        } else if ($$.isBubbleZType(data)) {
140,832✔
257
                                value = $$.getBubbleZData(value, "y");
252✔
258
                        }
259
                }
260

261
                return value;
155,508✔
262
        },
263

264
        /**
265
         * Get min/max value from the data
266
         * @private
267
         * @param {Array} data array data to be evaluated
268
         * @returns {{min: {number}, max: {number}}}
269
         */
270
        getMinMaxValue(data): {min: number, max: number} {
271
                const getBaseValue = this.getBaseValue.bind(this);
285✔
272
                let min;
273
                let max;
274

275
                (data || this.data.targets.map(t => t.values))
285!
276
                        .forEach((v, i) => {
277
                                const value = v.map(getBaseValue).filter(isNumber);
774✔
278

279
                                min = Math.min(i ? min : Infinity, ...value);
774✔
280
                                max = Math.max(i ? max : -Infinity, ...value);
774✔
281
                        });
282

283
                return {min, max};
285✔
284
        },
285

286
        /**
287
         * Get the min/max data
288
         * @private
289
         * @returns {{min: Array, max: Array}}
290
         */
291
        getMinMaxData(): {min: IDataRow[], max: IDataRow[]} {
292
                const $$ = this;
2,130✔
293
                const cacheKey = KEY.dataMinMax;
2,130✔
294
                let minMaxData = $$.cache.get(cacheKey);
2,130✔
295

296
                if (!minMaxData) {
2,130✔
297
                        const data = $$.data.targets.map(t => t.values);
774✔
298
                        const minMax = $$.getMinMaxValue(data);
285✔
299

300
                        let min = [];
285✔
301
                        let max = [];
285✔
302

303
                        // Cache the getFilteredDataByValue function calls
304
                        const {min: minVal, max: maxVal} = minMax;
285✔
305

306
                        data.forEach(v => {
285✔
307
                                const minData = $$.getFilteredDataByValue(v, minVal);
774✔
308
                                const maxData = $$.getFilteredDataByValue(v, maxVal);
774✔
309

310
                                if (minData.length) {
774✔
311
                                        min = min.concat(minData);
288✔
312
                                }
313

314
                                if (maxData.length) {
774✔
315
                                        max = max.concat(maxData);
285✔
316
                                }
317
                        });
318

319
                        // update the cached data
320
                        $$.cache.add(cacheKey, minMaxData = {min, max});
285✔
321
                }
322

323
                return minMaxData;
2,130✔
324
        },
325

326
        /**
327
         * Get sum of data per index
328
         * @private
329
         * @returns {Array}
330
         */
331
        getTotalPerIndex() {
332
                const $$ = this;
3,618✔
333
                const cacheKey = KEY.dataTotalPerIndex;
3,618✔
334
                let sum = $$.cache.get(cacheKey);
3,618✔
335

336
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
3,618!
337
                        sum = [];
3,618✔
338

339
                        $$.data.targets.forEach(row => {
3,618✔
340
                                row.values.forEach((v, i) => {
10,890✔
341
                                        if (!sum[i]) {
59,046✔
342
                                                sum[i] = 0;
19,938✔
343
                                        }
344

345
                                        sum[i] += ~~v.value;
59,046✔
346
                                });
347
                        });
348
                }
349

350
                return sum;
3,618✔
351
        },
352

353
        /**
354
         * Get total data sum
355
         * @param {boolean} subtractHidden Subtract hidden data from total
356
         * @returns {number}
357
         * @private
358
         */
359
        getTotalDataSum(subtractHidden) {
360
                const $$ = this;
2,712✔
361
                const cacheKey = KEY.dataTotalSum;
2,712✔
362
                let total = $$.cache.get(cacheKey);
2,712✔
363

364
                if (!isNumber(total)) {
2,712✔
365
                        total = $$.data.targets.reduce((acc, t) => {
345✔
366
                                return acc + t.values.reduce((sum, v) => sum + ~~v.value, 0);
1,302✔
367
                        }, 0);
368

369
                        $$.cache.add(cacheKey, total);
345✔
370
                }
371

372
                if (subtractHidden) {
2,712✔
373
                        total -= $$.getHiddenTotalDataSum();
1,839✔
374
                }
375

376
                return total;
2,712✔
377
        },
378

379
        /**
380
         * Get total hidden data sum
381
         * @returns {number}
382
         * @private
383
         */
384
        getHiddenTotalDataSum() {
385
                const $$ = this;
1,839✔
386
                const {api, state: {hiddenTargetIds}} = $$;
1,839✔
387
                let total = 0;
1,839✔
388

389
                if (hiddenTargetIds.length) {
1,839✔
390
                        total = api.data.values.bind(api)(hiddenTargetIds)
192✔
391
                                .reduce((p, c) => p + c);
12✔
392
                }
393

394
                return total;
1,839✔
395
        },
396

397
        /**
398
         * Get filtered data by value
399
         * @param {object} data Data
400
         * @param {number} value Value to be filtered
401
         * @returns {Array} filtered array data
402
         * @private
403
         */
404
        getFilteredDataByValue(data, value) {
405
                return data.filter(t => this.getBaseValue(t) === value);
3,240✔
406
        },
407

408
        /**
409
         * Return the max length of the data
410
         * @returns {number} max data length
411
         * @private
412
         */
413
        getMaxDataCount(): number {
414
                return Math.max(...this.data.targets.map(t => t.values.length), 0);
8,775✔
415
        },
416

417
        getMaxDataCountTarget() {
418
                let target = this.filterTargetsToShow() || [];
2,955!
419
                const length = target.length;
2,955✔
420
                const isInverted = this.config.axis_x_inverted;
2,955✔
421

422
                if (length > 1) {
2,955✔
423
                        target = target.map(t => t.values)
4,545✔
424
                                .reduce((a, b) => a.concat(b))
2,655✔
425
                                .map(v => v.x);
23,049✔
426

427
                        target = sortValue(getUnique(target))
1,890✔
428
                                .map((x, index, array) => ({
9,618✔
429
                                        x,
430
                                        index: isInverted ? array.length - index - 1 : index
9,618!
431
                                }));
432
                } else if (length) {
1,065✔
433
                        target = target[0].values.concat();
993✔
434
                }
435

436
                return target;
2,955✔
437
        },
438

439
        mapToIds(targets): string[] {
440
                return targets.map(d => d.id);
74,628✔
441
        },
442

443
        mapToTargetIds(ids) {
444
                const $$ = this;
3,309✔
445

446
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
3,309✔
447
        },
448

449
        hasTarget(targets, id): boolean {
450
                const ids = this.mapToIds(targets);
81✔
451

452
                for (let i = 0, val; (val = ids[i]); i++) {
81✔
453
                        if (val === id) {
168✔
454
                                return true;
81✔
455
                        }
456
                }
457

458
                return false;
×
459
        },
460

461
        isTargetToShow(targetId): boolean {
462
                return this.state.hiddenTargetIds.indexOf(targetId) < 0;
178,307✔
463
        },
464

465
        isLegendToShow(targetId): boolean {
466
                return this.state.hiddenLegendIds.indexOf(targetId) < 0;
19,140✔
467
        },
468

469
        filterTargetsToShow(targets?) {
470
                const $$ = this;
68,189✔
471

472
                return (targets || $$.data.targets).filter(t => $$.isTargetToShow(t.id));
161,180✔
473
        },
474

475
        mapTargetsToUniqueXs(targets) {
476
                const $$ = this;
10,872✔
477
                const {axis} = $$;
10,872✔
478
                let xs: any[] = [];
10,872✔
479

480
                if (targets?.length) {
10,872✔
481
                        xs = getUnique(
10,794✔
482
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
106,224✔
483
                        );
484

485
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
10,794✔
486
                }
487

488
                return sortValue(xs);
10,872✔
489
        },
490

491
        /**
492
         * Add to the state target Ids
493
         * @param {string} type State's prop name
494
         * @param {Array|string} targetIds Target ids array
495
         * @private
496
         */
497
        addTargetIds(type: string, targetIds: string[] | string): void {
498
                const {state} = this;
156✔
499
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as [];
156✔
500

501
                ids.forEach(v => {
156✔
502
                        state[type].indexOf(v) < 0 &&
207✔
503
                                state[type].push(v);
504
                });
505
        },
506

507
        /**
508
         * Remove from the state target Ids
509
         * @param {string} type State's prop name
510
         * @param {Array|string} targetIds Target ids array
511
         * @private
512
         */
513
        removeTargetIds(type: string, targetIds: string[] | string): void {
514
                const {state} = this;
165✔
515
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as [];
165!
516

517
                ids.forEach(v => {
165✔
518
                        const index = state[type].indexOf(v);
351✔
519

520
                        index >= 0 && state[type].splice(index, 1);
351✔
521
                });
522
        },
523

524
        addHiddenTargetIds(targetIds: string[]): void {
525
                this.addTargetIds("hiddenTargetIds", targetIds);
135✔
526
        },
527

528
        removeHiddenTargetIds(targetIds: string[]): void {
529
                this.removeTargetIds("hiddenTargetIds", targetIds);
54✔
530
        },
531

532
        addHiddenLegendIds(targetIds: string[]): void {
533
                this.addTargetIds("hiddenLegendIds", targetIds);
21✔
534
        },
535

536
        removeHiddenLegendIds(targetIds: string[]): void {
537
                this.removeTargetIds("hiddenLegendIds", targetIds);
111✔
538
        },
539

540
        getValuesAsIdKeyed(targets) {
541
                const $$ = this;
28,944✔
542
                const {hasAxis} = $$.state;
28,944✔
543
                const ys = {};
28,944✔
544
                const isMultipleX = $$.isMultipleX();
28,944✔
545
                const xs = isMultipleX ?
28,944✔
546
                        $$.mapTargetsToUniqueXs(targets)
547
                                .map(v => (isString(v) ? v : +v)) :
8,802!
548
                        null;
549

550
                targets.forEach(t => {
28,944✔
551
                        const data: any[] = [];
51,978✔
552

553
                        t.values
51,978✔
554
                                .filter(({value}) => isValue(value) || value === null)
273,762✔
555
                                .forEach(v => {
556
                                        let {value} = v;
273,762✔
557

558
                                        // exclude 'volume' value to correct mis domain calculation
559
                                        if (value !== null && $$.isCandlestickType(v)) {
273,762✔
560
                                                value = isArray(value) ?
360✔
561
                                                        value.slice(0, 4) :
562
                                                        [value.open, value.high, value.low, value.close];
563
                                        }
564

565
                                        if (isArray(value)) {
273,762✔
566
                                                data.push(...value);
2,178✔
567
                                        } else if (isObject(value) && "high" in value) {
271,584!
568
                                                data.push(...Object.values(value));
×
569
                                        } else if ($$.isBubbleZType(v)) {
271,584✔
570
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
540✔
571
                                        } else {
572
                                                if (isMultipleX) {
271,044✔
573
                                                        data[$$.getIndexByX(v.x, xs)] = value;
12,438✔
574
                                                } else {
575
                                                        data.push(value);
258,606✔
576
                                                }
577
                                        }
578
                                });
579

580
                        ys[t.id] = data;
51,978✔
581
                });
582

583
                return ys;
28,944✔
584
        },
585

586
        checkValueInTargets(targets, checker: Function): boolean {
587
                return Object.keys(targets)
9,078✔
588
                        .some(id => targets[id].values.some(v => checker(v.value)));
45,381✔
589
        },
590

591
        hasMultiTargets(): boolean {
592
                return this.filterTargetsToShow().length > 1;
240✔
593
        },
594

595
        hasNegativeValueInTargets(targets): boolean {
596
                return this.checkValueInTargets(targets, v => v < 0);
40,287✔
597
        },
598

599
        hasPositiveValueInTargets(targets): boolean {
600
                return this.checkValueInTargets(targets, v => v > 0);
5,094✔
601
        },
602

603
        /**
604
         * Sort targets data
605
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
606
         * @param {Array} targetsValue Target value
607
         * @returns {Array}
608
         * @private
609
         */
610
        orderTargets(targetsValue: IData[]): IData[] {
611
                const $$ = this;
10,833✔
612
                const targets = [...targetsValue];
10,833✔
613
                const fn = $$.getSortCompareFn();
10,833✔
614

615
                fn && targets.sort(fn);
10,833✔
616

617
                return targets;
10,833✔
618
        },
619

620
        /**
621
         * Get data.order compare function
622
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
623
         * @returns {Function} compare function
624
         * @private
625
         */
626
        getSortCompareFn(isReversed = false): Function | null {
10,833✔
627
                const $$ = this;
11,523✔
628
                const {config} = $$;
11,523✔
629
                const order = config.data_order;
11,523✔
630
                const orderAsc = /asc/i.test(order);
11,523✔
631
                const orderDesc = /desc/i.test(order);
11,523✔
632
                let fn;
633

634
                if (orderAsc || orderDesc) {
11,523✔
635
                        const reducer = (p, c) => p + Math.abs(c.value);
167,358✔
636
                        const sum = v => (isNumber(v) ? v : (
61,200✔
637
                                "values" in v ? v.values.reduce(reducer, 0) : v.value
60,720✔
638
                        ));
639

640
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
11,250✔
641
                                const t1Sum = sum(t1);
30,600✔
642
                                const t2Sum = sum(t2);
30,600✔
643

644
                                return isReversed ?
30,600✔
645
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
19,623!
646
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
10,977✔
647
                        };
648
                } else if (isFunction(order)) {
273!
649
                        fn = order.bind($$.api);
×
650
                }
651

652
                return fn || null;
11,523✔
653
        },
654

655
        filterByX(targets, x) {
656
                return mergeArray(targets.map(t => t.values)).filter(v => v.x - x === 0);
1,788✔
657
        },
658

659
        filterNullish(data) {
660
                const filter = v => isValue(v.value);
35,196✔
661

662
                return data ?
10,092!
663
                        data.filter(
664
                                v => "value" in v ? filter(v) : v.values.some(filter)
35,112✔
665
                        ) :
666
                        data;
667
        },
668

669
        filterRemoveNull(data) {
670
                return data.filter(d => isValue(this.getBaseValue(d)));
×
671
        },
672

673
        filterByXDomain(targets, xDomain) {
674
                return targets.map(t => ({
153✔
675
                        id: t.id,
676
                        id_org: t.id_org,
677
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,842✔
678
                }));
679
        },
680

681
        hasDataLabel() {
682
                const dataLabels = this.config.data_labels;
32,988✔
683

684
                return (isBoolean(dataLabels) && dataLabels) ||
32,988✔
685
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
686
        },
687

688
        /**
689
         * Determine if has null value
690
         * @param {Array} targets Data array to be evaluated
691
         * @returns {boolean}
692
         * @private
693
         */
694
        hasNullDataValue(targets: IDataRow[]): boolean {
695
                return targets.some(({value}) => value === null);
102✔
696
        },
697

698
        /**
699
         * Get data index from the event coodinates
700
         * @param {Event} event Event object
701
         * @returns {number}
702
         * @private
703
         */
704
        getDataIndexFromEvent(event): number {
705
                const $$ = this;
426✔
706
                const {
707
                        $el,
708
                        config,
709
                        state: {hasRadar, inputType, eventReceiver: {coords, rect}}
710
                } = $$;
426✔
711
                let index;
712

713
                if (hasRadar) {
426✔
714
                        let target = event.target;
12✔
715

716
                        // in case of multilined axis text
717
                        if (/tspan/i.test(target.tagName)) {
12!
718
                                target = target.parentNode;
×
719
                        }
720

721
                        const d: any = d3Select(target).datum();
12✔
722

723
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
12!
724
                } else {
725
                        const isRotated = config.axis_rotated;
414✔
726
                        const scrollPos = getScrollPosition($el.chart.node());
414✔
727

728
                        // get data based on the mouse coords
729
                        const e = inputType === "touch" && event.changedTouches ?
414✔
730
                                event.changedTouches[0] :
731
                                event;
732

733
                        let point = isRotated ? e.clientY + scrollPos.y : e.clientX + scrollPos.x;
414✔
734

735
                        if (hasViewBox($el.svg)) {
414!
736
                                const pos = [point, 0];
×
737

738
                                isRotated && pos.reverse();
×
739
                                point = getTransformCTM($el.eventRect.node(), ...pos)[isRotated ? "y" : "x"];
×
740
                        } else {
741
                                point -= isRotated ? rect.top : rect.left;
414✔
742
                        }
743

744
                        index = findIndex(
414✔
745
                                coords,
746
                                point,
747
                                0,
748
                                coords.length - 1,
749
                                isRotated
750
                        );
751
                }
752

753
                return index;
426✔
754
        },
755

756
        getDataLabelLength(min: number, max: number, key: "width" | "height"): number[] {
757
                const $$ = this;
519✔
758
                const paddingCoef = 1.3;
519✔
759

760
                return $$.getTextRect(
519✔
761
                        [min, max].map(v => $$.dataLabelFormat()(v))
1,038✔
762
                )?.map((rect: DOMRect) => rect[key] * paddingCoef) || [0, 0];
834✔
763
        },
764

765
        isNoneArc(d) {
766
                return this.hasTarget(this.data.targets, d.id);
×
767
        },
768

769
        isArc(d) {
770
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
771
        },
772

773
        findSameXOfValues(values, index) {
774
                const targetX = values[index].x;
×
775
                const sames: any[] = [];
×
776
                let i;
777

778
                for (i = index - 1; i >= 0; i--) {
×
779
                        if (targetX !== values[i].x) {
×
780
                                break;
×
781
                        }
782

783
                        sames.push(values[i]);
×
784
                }
785

786
                for (i = index; i < values.length; i++) {
×
787
                        if (targetX !== values[i].x) {
×
788
                                break;
×
789
                        }
790

791
                        sames.push(values[i]);
×
792
                }
793

794
                return sames;
×
795
        },
796

797
        findClosestFromTargets(targets, pos: [number, number]): IDataRow | undefined {
798
                const $$ = this;
63✔
799
                const candidates = targets.map(target => $$.findClosest(target.values, pos)); // map to array of closest points of each target
132✔
800

801
                // decide closest point and return
802
                return $$.findClosest(candidates, pos);
63✔
803
        },
804

805
        findClosest(values, pos: [number, number]): IDataRow | undefined {
806
                const $$ = this;
177✔
807
                const {$el: {main}} = $$;
177✔
808
                const data = values.filter(v => v && isValue(v.value));
780✔
809

810
                let minDist;
811
                let closest;
812

813
                // find mouseovering bar/candlestick
814
                // https://github.com/naver/billboard.js/issues/2434
815
                data
177✔
816
                        .filter(v => $$.isBarType(v.id) || $$.isCandlestickType(v.id))
726✔
817
                        .forEach(v => {
818
                                const selector = $$.isBarType(v.id) ?
6!
819
                                        `.${$BAR.chartBar}.${$COMMON.target}${
820
                                                $$.getTargetSelectorSuffix(v.id)
821
                                        } .${$BAR.bar}-${v.index}` :
822
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${
823
                                                $$.getTargetSelectorSuffix(v.id)
824
                                        } .${$CANDLESTICK.candlestick}-${v.index} path`;
825

826
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
6!
827
                                        closest = v;
6✔
828
                                }
829
                        });
830

831
                // find closest point from non-bar/candlestick
832
                data
177✔
833
                        .filter(v => !$$.isBarType(v.id) && !$$.isCandlestickType(v.id))
726✔
834
                        .forEach((v: IDataPoint) => {
835
                                const d = $$.dist(v, pos);
720✔
836

837
                                minDist = $$.getPointSensitivity(v);
720✔
838

839
                                if (d < minDist) {
720✔
840
                                        minDist = d;
120✔
841
                                        closest = v;
120✔
842
                                }
843
                        });
844

845
                return closest;
177✔
846
        },
847

848
        dist(data: IDataPoint, pos: [number, number]) {
849
                const $$ = this;
798✔
850
                const {config: {axis_rotated: isRotated}, scale} = $$;
798✔
851
                const xIndex = +isRotated; // true: 1, false: 0
798✔
852
                const yIndex = +!isRotated; // true: 0, false: 1
798✔
853
                const y = $$.circleY(data, data.index);
798✔
854
                const x = (scale.zoom || scale.x)(data.x);
798✔
855

856
                return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2));
798✔
857
        },
858

859
        /**
860
         * Convert data for step type
861
         * @param {Array} values Object data values
862
         * @returns {Array}
863
         * @private
864
         */
865
        convertValuesToStep(values) {
866
                const $$ = this;
684✔
867
                const {axis, config} = $$;
684✔
868
                const stepType = config.line_step_type;
684✔
869
                const isCategorized = axis ? axis.isCategorized() : false;
684!
870
                const converted = isArray(values) ? values.concat() : [values];
684!
871

872
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
684!
873
                        return values;
684✔
874
                }
875

876
                // when all datas are null, return empty array
877
                // https://github.com/naver/billboard.js/issues/3124
878
                if (converted.length) {
×
879
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
880
                        const head = converted[0];
×
881
                        const tail = converted[converted.length - 1];
×
882
                        const {id} = head;
×
883
                        let {x} = head;
×
884

885
                        // insert head
886
                        converted.unshift({x: --x, value: head.value, id});
×
887

888
                        isCategorized && stepType === "step-after" &&
×
889
                                converted.unshift({x: --x, value: head.value, id});
890

891
                        // append tail
892
                        x = tail.x;
×
893
                        converted.push({x: ++x, value: tail.value, id});
×
894

895
                        isCategorized && stepType === "step-before" &&
×
896
                                converted.push({x: ++x, value: tail.value, id});
897
                }
898

899
                return converted;
×
900
        },
901

902
        convertValuesToRange(values) {
903
                const converted = isArray(values) ? values.concat() : [values];
×
904
                const ranges: {x: string | number, id: string, value: number}[] = [];
×
905

906
                converted.forEach(range => {
×
907
                        const {x, id} = range;
×
908

909
                        ranges.push({
×
910
                                x,
911
                                id,
912
                                value: range.value[0]
913
                        });
914

915
                        ranges.push({
×
916
                                x,
917
                                id,
918
                                value: range.value[2]
919
                        });
920
                });
921

922
                return ranges;
×
923
        },
924

925
        updateDataAttributes(name, attrs) {
926
                const $$ = this;
21✔
927
                const {config} = $$;
21✔
928
                const current = config[`data_${name}`];
21✔
929

930
                if (isUndefined(attrs)) {
21✔
931
                        return current;
12✔
932
                }
933

934
                Object.keys(attrs).forEach(id => {
9✔
935
                        current[id] = attrs[id];
18✔
936
                });
937

938
                $$.redraw({withLegend: true});
9✔
939

940
                return current;
9✔
941
        },
942

943
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
1,386!
944
                const value = d?.value;
1,629✔
945

946
                if (isArray(value)) {
1,629✔
947
                        if (type === "bar") {
1,443!
948
                                return value.reduce((a, c) => c - a);
×
949
                        } else {
950
                                // @ts-ignore
951
                                const index = {
1,443✔
952
                                        areaRange: ["high", "mid", "low"],
953
                                        candlestick: ["open", "high", "low", "close", "volume"]
954
                                }[type].indexOf(key);
955

956
                                return index >= 0 && value ? value[index] : undefined;
1,443!
957
                        }
958
                } else if (value && key) {
186✔
959
                        return value[key];
30✔
960
                }
961

962
                return value;
156✔
963
        },
964

965
        /**
966
         * Set ratio for grouped data
967
         * @param {Array} data Data array
968
         * @private
969
         */
970
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
971
                const $$ = this;
837✔
972
                const {config} = $$;
837✔
973

974
                // calculate ratio if grouped data exists
975
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
837✔
976
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
3,618✔
977

978
                        data.forEach(v => {
237✔
979
                                "values" in v ? v.values.forEach(setter) : setter(v);
3,366✔
980
                        });
981
                }
982
        },
983

984
        /**
985
         * Get ratio value
986
         * @param {string} type Ratio for given type
987
         * @param {object} d Data value object
988
         * @param {boolean} asPercent Convert the return as percent or not
989
         * @returns {number} Ratio value
990
         * @private
991
         */
992
        getRatio(type: string, d, asPercent = false): number {
4,221✔
993
                const $$ = this;
7,839✔
994
                const {config, state} = $$;
7,839✔
995
                const api = $$.api;
7,839✔
996
                let ratio = 0;
7,839✔
997

998
                if (d && api.data.shown().length) {
7,839✔
999
                        ratio = d.ratio || d.value;
7,794✔
1000

1001
                        if (type === "arc") {
7,794✔
1002
                                // if has padAngle set, calculate rate based on value
1003
                                if ($$.pie.padAngle()()) {
1,788✔
1004
                                        ratio = d.value / $$.getTotalDataSum(true);
48✔
1005

1006
                                        // otherwise, based on the rendered angle value
1007
                                } else {
1008
                                        const gaugeArcLength = config.gauge_fullCircle ?
1,740✔
1009
                                                $$.getArcLength() :
1010
                                                $$.getStartingAngle() * -2;
1011
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
1,740✔
1012

1013
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
1,740✔
1014
                                }
1015
                        } else if (type === "index") {
6,006✔
1016
                                const dataValues = api.data.values.bind(api);
3,618✔
1017
                                let total = this.getTotalPerIndex();
3,618✔
1018

1019
                                if (state.hiddenTargetIds.length) {
3,618!
1020
                                        let hiddenSum = dataValues(state.hiddenTargetIds, false);
×
1021

1022
                                        if (hiddenSum.length) {
×
1023
                                                hiddenSum = hiddenSum
×
1024
                                                        .reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));
×
1025

1026
                                                total = total.map((v, i) => v - hiddenSum[i]);
×
1027
                                        }
1028
                                }
1029

1030
                                const divisor = total[d.index];
3,618✔
1031

1032
                                d.ratio = isNumber(d.value) && total && divisor ? d.value / divisor : 0;
3,618✔
1033

1034
                                ratio = d.ratio;
3,618✔
1035
                        } else if (type === "radar") {
2,388✔
1036
                                ratio = (
1,290✔
1037
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
1038
                                ) * config.radar_size_ratio;
1039
                        } else if (type === "bar") {
1,098✔
1040
                                const yScale = $$.getYScaleById.bind($$)(d.id);
156✔
1041
                                const max = yScale.domain().reduce((a, c) => c - a);
156✔
1042

1043
                                // when all data are 0, return 0
1044
                                ratio = max === 0 ? 0 : Math.abs(
156!
1045
                                        $$.getRangedData(d, null, type) / max
1046
                                );
1047
                        } else if (type === "treemap") {
942!
1048
                                ratio /= $$.getTotalDataSum(true);
942✔
1049
                        }
1050
                }
1051

1052
                return asPercent && ratio ? ratio * 100 : ratio;
7,839✔
1053
        },
1054

1055
        /**
1056
         * Sort data index to be aligned with x axis.
1057
         * @param {Array} tickValues Tick array values
1058
         * @private
1059
         */
1060
        updateDataIndexByX(tickValues) {
1061
                const $$ = this;
2,955✔
1062

1063
                const tickValueMap = tickValues.reduce((out, tick, index) => {
2,955✔
1064
                        out[Number(tick.x)] = index;
16,806✔
1065
                        return out;
16,806✔
1066
                }, {});
1067

1068
                $$.data.targets.forEach(t => {
2,955✔
1069
                        t.values.forEach((value, valueIndex) => {
5,670✔
1070
                                let index = tickValueMap[Number(value.x)];
30,753✔
1071

1072
                                if (index === undefined) {
30,753✔
1073
                                        index = valueIndex;
252✔
1074
                                }
1075
                                value.index = index;
30,753✔
1076
                        });
1077
                });
1078
        },
1079

1080
        /**
1081
         * Determine if bubble has dimension data
1082
         * @param {object|Array} d data value
1083
         * @returns {boolean}
1084
         * @private
1085
         */
1086
        isBubbleZType(d): boolean {
1087
                const $$ = this;
428,571✔
1088

1089
                return $$.isBubbleType(d) && (
428,571!
1090
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1091
                        (isArray(d.value) && d.value.length >= 2)
1092
                );
1093
        },
1094

1095
        /**
1096
         * Determine if bar has ranged data
1097
         * @param {Array} d data value
1098
         * @returns {boolean}
1099
         * @private
1100
         */
1101
        isBarRangeType(d): boolean {
1102
                const $$ = this;
27,684✔
1103
                const {value} = d;
27,684✔
1104

1105
                return $$.isBarType(d) && isArray(value) && value.length >= 2 &&
27,684✔
1106
                        value.every(isNumber);
1107
        },
1108

1109
        /**
1110
         * Get data object by id
1111
         * @param {string} id data id
1112
         * @returns {object}
1113
         * @private
1114
         */
1115
        getDataById(id: string) {
1116
                const d = this.cache.get(id) || this.api.data(id);
12,936!
1117

1118
                return d?.[0] ?? d;
12,936✔
1119
        }
1120
};
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