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

naver / billboard.js / 14657682663

25 Apr 2025 04:59AM UTC coverage: 94.12%. Remained the same
14657682663

push

github

netil
fix(data): Filter nullish data from rendering

Filter nullish data to be exclude on rendering & generation

Fix #3973

6297 of 6953 branches covered (90.57%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 3 files covered. (100.0%)

28 existing lines in 3 files now uncovered.

7836 of 8063 relevant lines covered (97.18%)

24430.22 hits per line

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

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

39
                return dataKey || existValue;
28,968✔
40
        },
41

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

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

49
                return !!(config.data_stack_normalize && config.data_groups.length);
116,447✔
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;
189,180✔
60

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

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

68
                return config.data_x ?
25,998✔
69
                        config.data_x :
70
                        (notEmpty(config.data_xs) ? config.data_xs[id] : null);
18,600✔
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;
21,363✔
96

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

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

105
                return id in $$.data.xs &&
180✔
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;
33✔
114
                const idsForX = Object.keys($$.data.xs);
33✔
115

116
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
33!
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 && (
86,508✔
141
                        notEmpty(this.config.data_xs) ||
142
                        this.hasType("bubble") ||
143
                        this.hasType("scatter")
144
                );
145
        },
146

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

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

157
                return data;
3,135✔
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) {
1,407✔
168
                const $$ = this;
1,467✔
169

170
                let value = $$.filterTargetsToShow($$.data.targets)
1,467✔
171
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
2,958✔
172

173
                if (filterNull) {
1,467✔
174
                        value = value.filter(v => v && "value" in v && isValue(v.value));
180✔
175
                }
176

177
                return value;
1,467✔
178
        },
179

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

183
                return valueOnIndex.length ? valueOnIndex[0] : null;
3,042✔
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;
72,624✔
208
                const {axis} = $$;
72,624✔
209
                let x = axis?.isCategorized() ? index : (rawX || index);
72,624✔
210

211
                if (axis?.isTimeSeries()) {
72,624✔
212
                        const fn = parseDate.bind($$);
12,702✔
213

214
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
12,702✔
215
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
59,922✔
216
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
12,384✔
217
                }
218

219
                return x;
72,624✔
220
        },
221

222
        updateXs(values): void {
223
                if (values.length) {
5,764✔
224
                        this.axis.xs = values.map(v => v.x);
37,283✔
225
                }
226
        },
227

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

231
                return isDefined(x) ? x : null;
66,856✔
232
        },
233

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

237
                return isDefined(x) ? x : null;
66,856✔
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;
294,073✔
248
                const {hasAxis} = $$.state;
294,073✔
249
                let {value} = data;
294,073✔
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) {
294,073✔
254
                        if ($$.isAreaRangeType(data)) {
273,306✔
255
                                value = $$.getRangedData(data, "mid");
2,997✔
256
                        } else if ($$.isBubbleZType(data)) {
270,309✔
257
                                value = $$.getBubbleZData(value, "y");
498✔
258
                        }
259
                }
260

261
                return value;
294,073✔
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);
390✔
272
                let min;
273
                let max;
274

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

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

283
                return {min, max};
390✔
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,946✔
293
                const cacheKey = KEY.dataMinMax;
2,946✔
294
                let minMaxData = $$.cache.get(cacheKey);
2,946✔
295

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

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

303
                        data.forEach(v => {
384✔
304
                                const minData = $$.getFilteredDataByValue(v, minMax.min);
975✔
305
                                const maxData = $$.getFilteredDataByValue(v, minMax.max);
975✔
306

307
                                if (minData.length) {
975✔
308
                                        min = min.concat(minData);
402✔
309
                                }
310

311
                                if (maxData.length) {
975✔
312
                                        max = max.concat(maxData);
384✔
313
                                }
314
                        });
315

316
                        // update the cached data
317
                        $$.cache.add(cacheKey, minMaxData = {min, max});
384✔
318
                }
319

320
                return minMaxData;
2,946✔
321
        },
322

323
        /**
324
         * Get sum of data per index
325
         * @private
326
         * @returns {Array}
327
         */
328
        getTotalPerIndex() {
329
                const $$ = this;
7,104✔
330
                const cacheKey = KEY.dataTotalPerIndex;
7,104✔
331
                let sum = $$.cache.get(cacheKey);
7,104✔
332

333
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
7,104!
334
                        sum = [];
7,104✔
335

336
                        $$.data.targets.forEach(row => {
7,104✔
337
                                row.values.forEach((v, i) => {
49,062✔
338
                                        if (!sum[i]) {
118,722✔
339
                                                sum[i] = 0;
33,228✔
340
                                        }
341

342
                                        sum[i] += isNumber(v.value) ? v.value : 0;
118,722✔
343
                                });
344
                        });
345
                }
346

347
                return sum;
7,104✔
348
        },
349

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

361
                if (!isNumber(total)) {
2,909✔
362
                        const sum = mergeArray($$.data.targets.map(t => t.values))
1,296✔
363
                                .map(v => v.value);
1,404✔
364

365
                        total = sum.length ? sum.reduce((p, c) => p + c) : 0;
1,032✔
366

367
                        $$.cache.add(cacheKey, total);
378✔
368
                }
369

370
                if (subtractHidden) {
2,909✔
371
                        total -= $$.getHiddenTotalDataSum();
1,949✔
372
                }
373

374
                return total;
2,909✔
375
        },
376

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

387
                if (hiddenTargetIds.length) {
1,949✔
388
                        total = api.data.values.bind(api)(hiddenTargetIds)
204✔
389
                                .reduce((p, c) => p + c);
12✔
390
                }
391

392
                return total;
1,949✔
393
        },
394

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

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

415
        getMaxDataCountTarget() {
416
                let target = this.filterTargetsToShow() || [];
5,764!
417
                const length = target.length;
5,764✔
418
                const isInverted = this.config.axis_x_inverted;
5,764✔
419

420
                if (length > 1) {
5,764✔
421
                        target = target.map(t => t.values)
9,320✔
422
                                .reduce((a, b) => a.concat(b))
5,968✔
423
                                .map(v => v.x);
41,809✔
424

425
                        target = sortValue(getUnique(target))
3,352✔
426
                                .map((x, index, array) => ({
17,003✔
427
                                        x,
428
                                        index: isInverted ? array.length - index - 1 : index
17,003!
429
                                }));
430
                } else if (length) {
2,412✔
431
                        target = target[0].values.concat();
2,277✔
432
                }
433

434
                return target;
5,764✔
435
        },
436

437
        mapToIds(targets): string[] {
438
                return targets.map(d => d.id);
157,770✔
439
        },
440

441
        mapToTargetIds(ids) {
442
                const $$ = this;
5,568✔
443

444
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
5,568✔
445
        },
446

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

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

456
                return false;
×
457
        },
458

459
        isTargetToShow(targetId): boolean {
460
                return this.state.hiddenTargetIds.indexOf(targetId) < 0;
341,283✔
461
        },
462

463
        isLegendToShow(targetId): boolean {
464
                return this.state.hiddenLegendIds.indexOf(targetId) < 0;
33,895✔
465
        },
466

467
        filterTargetsToShow(targets?) {
468
                const $$ = this;
125,374✔
469

470
                return (targets || $$.data.targets).filter(t => $$.isTargetToShow(t.id));
311,942✔
471
        },
472

473
        mapTargetsToUniqueXs(targets) {
474
                const $$ = this;
22,835✔
475
                const {axis} = $$;
22,835✔
476
                let xs: any[] = [];
22,835✔
477

478
                if (targets?.length) {
22,835✔
479
                        xs = getUnique(
22,679✔
480
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
256,112✔
481
                        );
482

483
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
55,338✔
484
                }
485

486
                return sortValue(xs);
22,835✔
487
        },
488

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

499
                ids.forEach(v => {
261✔
500
                        state[type].indexOf(v) < 0 &&
360✔
501
                                state[type].push(v);
502
                });
503
        },
504

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

515
                ids.forEach(v => {
201✔
516
                        const index = state[type].indexOf(v);
414✔
517

518
                        index >= 0 && state[type].splice(index, 1);
414✔
519
                });
520
        },
521

522
        addHiddenTargetIds(targetIds: string[]): void {
523
                this.addTargetIds("hiddenTargetIds", targetIds);
240✔
524
        },
525

526
        removeHiddenTargetIds(targetIds: string[]): void {
527
                this.removeTargetIds("hiddenTargetIds", targetIds);
87✔
528
        },
529

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

534
        removeHiddenLegendIds(targetIds: string[]): void {
535
                this.removeTargetIds("hiddenLegendIds", targetIds);
114✔
536
        },
537

538
        getValuesAsIdKeyed(targets) {
539
                const $$ = this;
61,746✔
540
                const {hasAxis} = $$.state;
61,746✔
541
                const ys = {};
61,746✔
542
                const isMultipleX = $$.isMultipleX();
61,746✔
543
                const xs = isMultipleX ?
61,746✔
544
                        $$.mapTargetsToUniqueXs(targets)
545
                                .map(v => (isString(v) ? v : +v)) :
14,004!
546
                        null;
547

548
                targets.forEach(t => {
61,746✔
549
                        const data: any[] = [];
117,936✔
550

551
                        t.values
117,936✔
552
                                .filter(({value}) => isValue(value) || value === null)
679,386✔
553
                                .forEach(v => {
554
                                        let {value} = v;
679,386✔
555

556
                                        // exclude 'volume' value to correct mis domain calculation
557
                                        if (value !== null && $$.isCandlestickType(v)) {
679,386✔
558
                                                value = isArray(value) ?
1,092✔
559
                                                        value.slice(0, 4) :
560
                                                        [value.open, value.high, value.low, value.close];
561
                                        }
562

563
                                        if (isArray(value)) {
679,386✔
564
                                                data.push(...value);
5,880✔
565
                                        } else if (isObject(value) && "high" in value) {
673,506✔
566
                                                data.push(...Object.values(value));
720✔
567
                                        } else if ($$.isBubbleZType(v)) {
672,786✔
568
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
540✔
569
                                        } else {
570
                                                if (isMultipleX) {
672,246✔
571
                                                        data[$$.getIndexByX(v.x, xs)] = value;
21,078✔
572
                                                } else {
573
                                                        data.push(value);
651,168✔
574
                                                }
575
                                        }
576
                                });
577

578
                        ys[t.id] = data;
117,936✔
579
                });
580

581
                return ys;
61,746✔
582
        },
583

584
        checkValueInTargets(targets, checker: Function): boolean {
585
                const ids = Object.keys(targets);
17,436✔
586
                let values;
587

588
                for (let i = 0; i < ids.length; i++) {
17,436✔
589
                        values = targets[ids[i]].values;
33,741✔
590

591
                        for (let j = 0; j < values.length; j++) {
33,741✔
592
                                if (checker(values[j].value)) {
96,246✔
593
                                        return true;
9,630✔
594
                                }
595
                        }
596
                }
597

598
                return false;
7,806✔
599
        },
600

601
        hasMultiTargets(): boolean {
602
                return this.filterTargetsToShow().length > 1;
258✔
603
        },
604

605
        hasNegativeValueInTargets(targets): boolean {
606
                return this.checkValueInTargets(targets, v => v < 0);
83,943✔
607
        },
608

609
        hasPositiveValueInTargets(targets): boolean {
610
                return this.checkValueInTargets(targets, v => v > 0);
12,303✔
611
        },
612

613
        /**
614
         * Sort targets data
615
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
616
         * @param {Array} targetsValue Target value
617
         * @returns {Array}
618
         * @private
619
         */
620
        orderTargets(targetsValue: IData[]): IData[] {
621
                const $$ = this;
22,366✔
622
                const targets = [...targetsValue];
22,366✔
623
                const fn = $$.getSortCompareFn();
22,366✔
624

625
                fn && targets.sort(fn);
22,366✔
626

627
                return targets;
22,366✔
628
        },
629

630
        /**
631
         * Get data.order compare function
632
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
633
         * @returns {Function} compare function
634
         * @private
635
         */
636
        getSortCompareFn(isReversed = false): Function | null {
22,366✔
637
                const $$ = this;
23,119✔
638
                const {config} = $$;
23,119✔
639
                const order = config.data_order;
23,119✔
640
                const orderAsc = /asc/i.test(order);
23,119✔
641
                const orderDesc = /desc/i.test(order);
23,119✔
642
                let fn;
643

644
                if (orderAsc || orderDesc) {
23,119✔
645
                        const reducer = (p, c) => p + Math.abs(c.value);
316,859✔
646
                        const sum = v => (isNumber(v) ? v : (
116,034✔
647
                                "values" in v ? v.values.reduce(reducer, 0) : v.value
115,524✔
648
                        ));
649

650
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
22,447✔
651
                                const t1Sum = sum(t1);
58,017✔
652
                                const t2Sum = sum(t2);
58,017✔
653

654
                                return isReversed ?
58,017✔
655
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
26,573!
656
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
31,444✔
657
                        };
658
                } else if (isFunction(order)) {
672✔
659
                        fn = order.bind($$.api);
54✔
660
                }
661

662
                return fn || null;
23,119✔
663
        },
664

665
        filterByX(targets, x) {
666
                return mergeArray(targets.map(t => t.values)).filter(v => v.x - x === 0);
3,108✔
667
        },
668

669
        filterNullish(data) {
670
                const filter = v => isValue(v.value);
68,475✔
671

672
                return data ?
19,170!
673
                        data.filter(
674
                                v => "value" in v ? filter(v) : v.values.some(filter)
68,214✔
675
                        ) :
676
                        data;
677
        },
678

679
        filterRemoveNull(data) {
680
                return data.filter(d => isValue(this.getBaseValue(d)));
276✔
681
        },
682

683
        filterByXDomain(targets, xDomain) {
684
                return targets.map(t => ({
153✔
685
                        id: t.id,
686
                        id_org: t.id_org,
687
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,842✔
688
                }));
689
        },
690

691
        hasDataLabel() {
692
                const dataLabels = this.config.data_labels;
68,833✔
693

694
                return (isBoolean(dataLabels) && dataLabels) ||
68,833✔
695
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
696
        },
697

698
        /**
699
         * Determine if has null value
700
         * @param {Array} targets Data array to be evaluated
701
         * @returns {boolean}
702
         * @private
703
         */
704
        hasNullDataValue(targets: IDataRow[]): boolean {
705
                return targets.some(({value}) => value === null);
276✔
706
        },
707

708
        /**
709
         * Get data index from the event coodinates
710
         * @param {Event} event Event object
711
         * @returns {number}
712
         * @private
713
         */
714
        getDataIndexFromEvent(event): number {
715
                const $$ = this;
1,524✔
716
                const {
717
                        $el,
718
                        config,
719
                        state: {hasRadar, inputType, eventReceiver: {coords, rect}}
720
                } = $$;
1,524✔
721
                let index;
722

723
                if (hasRadar) {
1,524✔
724
                        let target = event.target;
33✔
725

726
                        // in case of multilined axis text
727
                        if (/tspan/i.test(target.tagName)) {
33!
UNCOV
728
                                target = target.parentNode;
×
729
                        }
730

731
                        const d: any = d3Select(target).datum();
33✔
732

733
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
33!
734
                } else {
735
                        const isRotated = config.axis_rotated;
1,491✔
736
                        const scrollPos = getScrollPosition($el.chart.node());
1,491✔
737

738
                        // get data based on the mouse coords
739
                        const e = inputType === "touch" && event.changedTouches ?
1,491✔
740
                                event.changedTouches[0] :
741
                                event;
742

743
                        let point = isRotated ? e.clientY + scrollPos.y : e.clientX + scrollPos.x;
1,491✔
744

745
                        if (hasViewBox($el.svg)) {
1,491✔
746
                                const pos = [point, 0];
39✔
747

748
                                isRotated && pos.reverse();
39✔
749
                                point = getTransformCTM($el.eventRect.node(), ...pos)[isRotated ? "y" : "x"];
39✔
750
                        } else {
751
                                point -= isRotated ? rect.top : rect.left;
1,452✔
752
                        }
753

754
                        index = findIndex(
1,491✔
755
                                coords,
756
                                point,
757
                                0,
758
                                coords.length - 1,
759
                                isRotated
760
                        );
761
                }
762

763
                return index;
1,524✔
764
        },
765

766
        getDataLabelLength(min, max, key) {
767
                const $$ = this;
2,586✔
768
                const lengths = [0, 0];
2,586✔
769
                const paddingCoef = 1.3;
2,586✔
770

771
                $$.$el.chart.select("svg").selectAll(".dummy")
2,586✔
772
                        .data([min, max])
773
                        .enter()
774
                        .append("text")
775
                        .text(d => $$.dataLabelFormat(d.id)(d))
4,134✔
776
                        .each(function(d, i) {
777
                                lengths[i] = this.getBoundingClientRect()[key] * paddingCoef;
4,134✔
778
                        })
779
                        .remove();
780

781
                return lengths;
2,586✔
782
        },
783

784
        isNoneArc(d) {
UNCOV
785
                return this.hasTarget(this.data.targets, d.id);
×
786
        },
787

788
        isArc(d) {
UNCOV
789
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
790
        },
791

792
        findSameXOfValues(values, index) {
UNCOV
793
                const targetX = values[index].x;
×
UNCOV
794
                const sames: any[] = [];
×
795
                let i;
796

UNCOV
797
                for (i = index - 1; i >= 0; i--) {
×
UNCOV
798
                        if (targetX !== values[i].x) {
×
799
                                break;
×
800
                        }
801

UNCOV
802
                        sames.push(values[i]);
×
803
                }
804

UNCOV
805
                for (i = index; i < values.length; i++) {
×
UNCOV
806
                        if (targetX !== values[i].x) {
×
807
                                break;
×
808
                        }
809

UNCOV
810
                        sames.push(values[i]);
×
811
                }
812

UNCOV
813
                return sames;
×
814
        },
815

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

820
                // decide closest point and return
821
                return $$.findClosest(candidates, pos);
117✔
822
        },
823

824
        findClosest(values, pos: [number, number]): IDataRow | undefined {
825
                const $$ = this;
306✔
826
                const {$el: {main}} = $$;
306✔
827
                const data = values.filter(v => v && isValue(v.value));
1,104✔
828

829
                let minDist;
830
                let closest;
831

832
                // find mouseovering bar/candlestick
833
                // https://github.com/naver/billboard.js/issues/2434
834
                data
306✔
835
                        .filter(v => $$.isBarType(v.id) || $$.isCandlestickType(v.id))
1,005✔
836
                        .forEach(v => {
837
                                const selector = $$.isBarType(v.id) ?
30✔
838
                                        `.${$BAR.chartBar}.${$COMMON.target}${
839
                                                $$.getTargetSelectorSuffix(v.id)
840
                                        } .${$BAR.bar}-${v.index}` :
841
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${
842
                                                $$.getTargetSelectorSuffix(v.id)
843
                                        } .${$CANDLESTICK.candlestick}-${v.index} path`;
844

845
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
30✔
846
                                        closest = v;
6✔
847
                                }
848
                        });
849

850
                // find closest point from non-bar/candlestick
851
                data
306✔
852
                        .filter(v => !$$.isBarType(v.id) && !$$.isCandlestickType(v.id))
1,005✔
853
                        .forEach((v: IDataPoint) => {
854
                                const d = $$.dist(v, pos);
975✔
855

856
                                minDist = $$.getPointSensitivity(v);
975✔
857

858
                                if (d < minDist) {
975✔
859
                                        minDist = d;
216✔
860
                                        closest = v;
216✔
861
                                }
862
                        });
863

864
                return closest;
306✔
865
        },
866

867
        dist(data: IDataPoint, pos: [number, number]) {
868
                const $$ = this;
1,101✔
869
                const {config: {axis_rotated: isRotated}, scale} = $$;
1,101✔
870
                const xIndex = +isRotated; // true: 1, false: 0
1,101✔
871
                const yIndex = +!isRotated; // true: 0, false: 1
1,101✔
872
                const y = $$.circleY(data, data.index);
1,101✔
873
                const x = (scale.zoom || scale.x)(data.x);
1,101✔
874

875
                return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2));
1,101✔
876
        },
877

878
        /**
879
         * Convert data for step type
880
         * @param {Array} values Object data values
881
         * @returns {Array}
882
         * @private
883
         */
884
        convertValuesToStep(values) {
885
                const $$ = this;
1,752✔
886
                const {axis, config} = $$;
1,752✔
887
                const stepType = config.line_step_type;
1,752✔
888
                const isCategorized = axis ? axis.isCategorized() : false;
1,752!
889
                const converted = isArray(values) ? values.concat() : [values];
1,752!
890

891
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
1,752✔
892
                        return values;
936✔
893
                }
894

895
                // when all datas are null, return empty array
896
                // https://github.com/naver/billboard.js/issues/3124
897
                if (converted.length) {
816!
898
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
899
                        const head = converted[0];
816✔
900
                        const tail = converted[converted.length - 1];
816✔
901
                        const {id} = head;
816✔
902
                        let {x} = head;
816✔
903

904
                        // insert head
905
                        converted.unshift({x: --x, value: head.value, id});
816✔
906

907
                        isCategorized && stepType === "step-after" &&
816✔
908
                                converted.unshift({x: --x, value: head.value, id});
909

910
                        // append tail
911
                        x = tail.x;
816✔
912
                        converted.push({x: ++x, value: tail.value, id});
816✔
913

914
                        isCategorized && stepType === "step-before" &&
816✔
915
                                converted.push({x: ++x, value: tail.value, id});
916
                }
917

918
                return converted;
816✔
919
        },
920

921
        convertValuesToRange(values) {
UNCOV
922
                const converted = isArray(values) ? values.concat() : [values];
×
UNCOV
923
                const ranges: {x: string | number, id: string, value: number}[] = [];
×
924

UNCOV
925
                converted.forEach(range => {
×
UNCOV
926
                        const {x, id} = range;
×
927

UNCOV
928
                        ranges.push({
×
929
                                x,
930
                                id,
931
                                value: range.value[0]
932
                        });
933

UNCOV
934
                        ranges.push({
×
935
                                x,
936
                                id,
937
                                value: range.value[2]
938
                        });
939
                });
940

UNCOV
941
                return ranges;
×
942
        },
943

944
        updateDataAttributes(name, attrs) {
945
                const $$ = this;
21✔
946
                const {config} = $$;
21✔
947
                const current = config[`data_${name}`];
21✔
948

949
                if (isUndefined(attrs)) {
21✔
950
                        return current;
12✔
951
                }
952

953
                Object.keys(attrs).forEach(id => {
9✔
954
                        current[id] = attrs[id];
18✔
955
                });
956

957
                $$.redraw({withLegend: true});
9✔
958

959
                return current;
9✔
960
        },
961

962
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
3,843!
963
                const value = d?.value;
5,427✔
964

965
                if (isArray(value)) {
5,427✔
966
                        if (type === "bar") {
3,198✔
967
                                return value.reduce((a, c) => c - a);
24✔
968
                        } else {
969
                                // @ts-ignore
970
                                const index = {
3,174✔
971
                                        areaRange: ["high", "mid", "low"],
972
                                        candlestick: ["open", "high", "low", "close", "volume"]
973
                                }[type].indexOf(key);
974

975
                                return index >= 0 && value ? value[index] : undefined;
3,174!
976
                        }
977
                } else if (value && key) {
2,229✔
978
                        return value[key];
813✔
979
                }
980

981
                return value;
1,416✔
982
        },
983

984
        /**
985
         * Set ratio for grouped data
986
         * @param {Array} data Data array
987
         * @private
988
         */
989
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
990
                const $$ = this;
2,133✔
991
                const {config} = $$;
2,133✔
992

993
                // calculate ratio if grouped data exists
994
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
2,133✔
995
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
6,024✔
996

997
                        data.forEach(v => {
474✔
998
                                "values" in v ? v.values.forEach(setter) : setter(v);
5,427✔
999
                        });
1000
                }
1001
        },
1002

1003
        /**
1004
         * Get ratio value
1005
         * @param {string} type Ratio for given type
1006
         * @param {object} d Data value object
1007
         * @param {boolean} asPercent Convert the return as percent or not
1008
         * @returns {number} Ratio value
1009
         * @private
1010
         */
1011
        getRatio(type: string, d, asPercent = false): number {
5,880✔
1012
                const $$ = this;
12,984✔
1013
                const {config, state} = $$;
12,984✔
1014
                const api = $$.api;
12,984✔
1015
                let ratio = 0;
12,984✔
1016

1017
                if (d && api.data.shown().length) {
12,984✔
1018
                        ratio = d.ratio || d.value;
12,936✔
1019

1020
                        if (type === "arc") {
12,936✔
1021
                                // if has padAngle set, calculate rate based on value
1022
                                if ($$.pie.padAngle()()) {
1,977✔
1023
                                        ratio = d.value / $$.getTotalDataSum(true);
48✔
1024

1025
                                        // otherwise, based on the rendered angle value
1026
                                } else {
1027
                                        const gaugeArcLength = config.gauge_fullCircle ?
1,929✔
1028
                                                $$.getArcLength() :
1029
                                                $$.getStartingAngle() * -2;
1030
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
1,929✔
1031

1032
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
1,929✔
1033
                                }
1034
                        } else if (type === "index") {
10,959✔
1035
                                const dataValues = api.data.values.bind(api);
7,104✔
1036
                                let total = this.getTotalPerIndex();
7,104✔
1037

1038
                                if (state.hiddenTargetIds.length) {
7,104✔
1039
                                        let hiddenSum = dataValues(state.hiddenTargetIds, false);
963✔
1040

1041
                                        if (hiddenSum.length) {
963✔
1042
                                                hiddenSum = hiddenSum
195✔
1043
                                                        .reduce((acc, curr) =>
1044
                                                                acc.map((v, i) => (isNumber(v) ? v : 0) + curr[i])
144!
1045
                                                        );
1046

1047
                                                total = total.map((v, i) => v - hiddenSum[i]);
495✔
1048
                                        }
1049
                                }
1050

1051
                                const divisor = total[d.index];
7,104✔
1052

1053
                                d.ratio = isNumber(d.value) && total && divisor ? d.value / divisor : 0;
7,104✔
1054

1055
                                ratio = d.ratio;
7,104✔
1056
                        } else if (type === "radar") {
3,855✔
1057
                                ratio = (
1,506✔
1058
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
1059
                                ) * config.radar_size_ratio;
1060
                        } else if (type === "bar") {
2,349✔
1061
                                const yScale = $$.getYScaleById.bind($$)(d.id);
1,347✔
1062
                                const max = yScale.domain().reduce((a, c) => c - a);
1,347✔
1063

1064
                                // when all data are 0, return 0
1065
                                ratio = max === 0 ? 0 : Math.abs(
1,347✔
1066
                                        $$.getRangedData(d, null, type) / max
1067
                                );
1068
                        } else if (type === "treemap") {
1,002!
1069
                                ratio /= $$.getTotalDataSum(true);
1,002✔
1070
                        }
1071
                }
1072

1073
                return asPercent && ratio ? ratio * 100 : ratio;
12,984✔
1074
        },
1075

1076
        /**
1077
         * Sort data index to be aligned with x axis.
1078
         * @param {Array} tickValues Tick array values
1079
         * @private
1080
         */
1081
        updateDataIndexByX(tickValues) {
1082
                const $$ = this;
5,764✔
1083

1084
                const tickValueMap = tickValues.reduce((out, tick, index) => {
5,764✔
1085
                        out[Number(tick.x)] = index;
37,283✔
1086
                        return out;
37,283✔
1087
                }, {});
1088

1089
                $$.data.targets.forEach(t => {
5,764✔
1090
                        t.values.forEach((value, valueIndex) => {
11,837✔
1091
                                let index = tickValueMap[Number(value.x)];
63,403✔
1092

1093
                                if (index === undefined) {
63,403✔
1094
                                        index = valueIndex;
870✔
1095
                                }
1096
                                value.index = index;
63,403✔
1097
                        });
1098
                });
1099
        },
1100

1101
        /**
1102
         * Determine if bubble has dimension data
1103
         * @param {object|Array} d data value
1104
         * @returns {boolean}
1105
         * @private
1106
         */
1107
        isBubbleZType(d): boolean {
1108
                const $$ = this;
981,093✔
1109

1110
                return $$.isBubbleType(d) && (
981,093!
1111
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1112
                        (isArray(d.value) && d.value.length >= 2)
1113
                );
1114
        },
1115

1116
        /**
1117
         * Determine if bar has ranged data
1118
         * @param {Array} d data value
1119
         * @returns {boolean}
1120
         * @private
1121
         */
1122
        isBarRangeType(d): boolean {
1123
                const $$ = this;
69,894✔
1124
                const {value} = d;
69,894✔
1125

1126
                return $$.isBarType(d) && isArray(value) && value.length >= 2 &&
69,894✔
1127
                        value.every(v => isNumber(v));
1,176✔
1128
        },
1129

1130
        /**
1131
         * Get data object by id
1132
         * @param {string} id data id
1133
         * @returns {object}
1134
         * @private
1135
         */
1136
        getDataById(id: string) {
1137
                const d = this.cache.get(id) || this.api.data(id);
22,972✔
1138

1139
                return d?.[0] ?? d;
22,972✔
1140
        }
1141
};
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