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

naver / billboard.js / 6079269130

05 Sep 2023 01:40AM UTC coverage: 94.239% (-0.02%) from 94.261%
6079269130

push

github

netil
fix(shape): Fix circleY() undefined error

- Make .circleY() visible and included as part of common
- Move .circleY() to shape.ts file

Fix #3388

5992 of 6639 branches covered (0.0%)

Branch coverage included in aggregate %.

10 of 12 new or added lines in 2 files covered. (83.33%)

22 existing lines in 2 files now uncovered.

7732 of 7924 relevant lines covered (97.58%)

21314.59 hits per line

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

92.64
/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 type {IData, IDataPoint, IDataRow} from "./IData";
9
import {
10
        findIndex,
11
        getUnique,
12
        hasValue,
13
        isArray,
14
        isboolean,
15
        isDefined,
16
        isFunction,
17
        isNumber,
18
        isObject,
19
        isObjectType,
20
        isString,
21
        isUndefined,
22
        isValue,
23
        mergeArray,
24
        notEmpty,
25
        parseDate,
26
        sortValue
27
} from "../../module/util";
28

29
export default {
30
        isX(key) {
31
                const $$ = this;
25,890✔
32
                const {config} = $$;
25,890✔
33
                const dataKey = config.data_x && key === config.data_x;
25,890✔
34
                const existValue = notEmpty(config.data_xs) && hasValue(config.data_xs, key);
25,890✔
35

36
                return dataKey || existValue;
25,890✔
37
        },
38

39
        isNotX(key): boolean {
40
                return !this.isX(key);
12,945✔
41
        },
42

43
        isStackNormalized(): boolean {
44
                const {config} = this;
137,877✔
45

46
                return !!(config.data_stack_normalize && config.data_groups.length);
137,877✔
47
        },
48

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

58
                return id ?
217,167✔
59
                        groups.some(v => v.indexOf(id) >= 0 && v.length > 1) :
45,489!
60
                        groups.length > 0;
61
        },
62

63
        getXKey(id) {
64
                const $$ = this;
23,226✔
65
                const {config} = $$;
23,226✔
66

67
                return config.data_x ?
23,226✔
68
                        config.data_x : (notEmpty(config.data_xs) ? config.data_xs[id] : null);
39,552✔
69
        },
70

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

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

82
                return xValues;
3✔
83
        },
84

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

95
                return basedX ?
25,311✔
96
                        basedX.indexOf(isString(x) ? x : +x) :
50,439!
97
                        ($$.filterByX($$.data.targets, x)[0] || {index: null}).index;
186✔
98
        },
99

100
        getXValue(id: string, i: number): number {
101
                const $$ = this;
150✔
102

103
                return id in $$.data.xs &&
150✔
104
                        $$.data.xs[id] &&
105
                        isValue($$.data.xs[id][i]) ? $$.data.xs[id][i] : i;
150✔
106
        },
107

108
        getOtherTargetXs(): string | null {
109
                const $$ = this;
33✔
110
                const idsForX = Object.keys($$.data.xs);
33✔
111

112
                return idsForX.length ? $$.data.xs[idsForX[0]] : null;
33!
113
        },
114

115
        getOtherTargetX(index: number): string | null {
116
                const xs = this.getOtherTargetXs();
27✔
117

118
                return xs && index < xs.length ? xs[index] : null;
27!
119
        },
120

121
        addXs(xs): void {
6✔
122
                const $$ = this;
6✔
123
                const {config} = $$;
6✔
124

125
                Object.keys(xs).forEach(id => {
6✔
126
                        config.data_xs[id] = xs[id];
6✔
127
                });
128
        },
129

130
        isMultipleX(): boolean {
131
                return notEmpty(this.config.data_xs) ||
93,108✔
132
                        this.hasType("bubble") ||
133
                        this.hasType("scatter");
134
        },
135

136
        addName(data) {
137
                const $$ = this;
2,880✔
138
                const {config} = $$;
2,880✔
139
                let name;
140

141
                if (data) {
2,880✔
142
                        name = config.data_names[data.id];
2,859✔
143
                        data.name = name !== undefined ? name : data.id;
2,859!
144
                }
145

146
                return data;
2,880✔
147
        },
148

149
        /**
150
         * Get all values on given index
151
         * @param {number} index Index
152
         * @param {boolean} filterNull Filter nullish value
153
         * @returns {Array}
154
         * @private
155
         */
156
        getAllValuesOnIndex(index: number, filterNull = false) {
1,335✔
157
                const $$ = this;
1,335✔
158

159
                let value = $$.filterTargetsToShow($$.data.targets)
1,335✔
160
                        .map(t => $$.addName($$.getValueOnIndex(t.values, index)));
2,733✔
161

162
                if (filterNull) {
1,335✔
163
                        value = value.filter(v => v && "value" in v && isValue(v.value));
288✔
164
                }
165

166
                return value;
1,335✔
167
        },
168

169
        getValueOnIndex(values, index: number) {
2,817✔
170
                const valueOnIndex = values.filter(v => v.index === index);
13,491✔
171

172
                return valueOnIndex.length ? valueOnIndex[0] : null;
2,817✔
173
        },
174

175
        updateTargetX(targets, x) {
9✔
176
                const $$ = this;
9✔
177

178
                targets.forEach(t => {
24✔
179
                        t.values.forEach((v, i) => {
36✔
180
                                v.x = $$.generateTargetX(x[i], t.id, i);
36✔
181
                        });
182

183
                        $$.data.xs[t.id] = x;
12✔
184
                });
185
        },
186

187
        updateTargetXs(targets, xs) {
3✔
188
                const $$ = this;
3✔
189

190
                targets.forEach(t => {
6✔
191
                        xs[t.id] && $$.updateTargetX([t], xs[t.id]);
6✔
192
                });
193
        },
194

195
        generateTargetX(rawX, id: string, index: number) {
196
                const $$ = this;
65,931✔
197
                const {axis} = $$;
65,931✔
198
                let x = axis?.isCategorized() ? index : (rawX || index);
65,931✔
199

200
                if (axis?.isTimeSeries()) {
65,931✔
201
                        const fn = parseDate.bind($$);
10,626✔
202

203
                        x = rawX ? fn(rawX) : fn($$.getXValue(id, index));
10,626✔
204
                } else if (axis?.isCustomX() && !axis?.isCategorized()) {
55,305✔
205
                        x = isValue(rawX) ? +rawX : $$.getXValue(id, index);
12,156✔
206
                }
207

208
                return x;
65,931✔
209
        },
210

211
        updateXs(values): void {
6,126✔
212
                if (values.length) {
6,126✔
213
                        this.axis.xs = values.map(v => v.x);
41,106✔
214
                }
215
        },
216

217
        getPrevX(i: number): number[] | null {
218
                const x = this.axis.xs[i - 1];
73,176✔
219

220
                return isDefined(x) ? x : null;
73,176✔
221
        },
222

223
        getNextX(i: number): number[] | null {
224
                const x = this.axis.xs[i + 1];
73,176✔
225

226
                return isDefined(x) ? x : null;
73,176✔
227
        },
228

229
        /**
230
         * Get base value isAreaRangeType
231
         * @param {object} data Data object
232
         * @returns {number}
233
         * @private
234
         */
235
        getBaseValue(data): number {
236
                const $$ = this;
314,034✔
237
                const {hasAxis} = $$.state;
314,034✔
238
                let {value} = data;
314,034✔
239

240
                // In case of area-range, data is given as: [low, mid, high] or {low, mid, high}
241
                // will take the 'mid' as the base value
242
                if (value && hasAxis) {
314,034✔
243
                        if ($$.isAreaRangeType(data)) {
293,055✔
244
                                value = $$.getRangedData(data, "mid");
2,418✔
245
                        } else if ($$.isBubbleZType(data)) {
290,637✔
246
                                value = $$.getBubbleZData(value, "y");
498✔
247
                        }
248
                }
249

250
                return value;
314,034✔
251
        },
252

253
        /**
254
         * Get min/max value from the data
255
         * @private
256
         * @param {Array} data array data to be evaluated
257
         * @returns {{min: {number}, max: {number}}}
258
         */
259
        getMinMaxValue(data): {min: number, max: number} {
339✔
260
                const getBaseValue = this.getBaseValue.bind(this);
339✔
261
                let min;
262
                let max;
263

264
                (data || this.data.targets.map(t => t.values))
339✔
265
                        .forEach((v, i) => {
906✔
266
                                const value = v.map(getBaseValue).filter(isNumber);
906✔
267

268
                                min = Math.min(i ? min : Infinity, ...value);
906✔
269
                                max = Math.max(i ? max : -Infinity, ...value);
906✔
270
                        });
271

272
                return {min, max};
339✔
273
        },
274

275
        /**
276
         * Get the min/max data
277
         * @private
278
         * @returns {{min: Array, max: Array}}
279
         */
280
        getMinMaxData(): {min: IDataRow[], max: IDataRow[]} {
2,634✔
281
                const $$ = this;
2,634✔
282
                const cacheKey = KEY.dataMinMax;
2,634✔
283
                let minMaxData = $$.cache.get(cacheKey);
2,634✔
284

285
                if (!minMaxData) {
2,634✔
286
                        const data = $$.data.targets.map(t => t.values);
888✔
287
                        const minMax = $$.getMinMaxValue(data);
333✔
288

289
                        let min = [];
333✔
290
                        let max = [];
333✔
291

292
                        data.forEach(v => {
888✔
293
                                const minData = $$.getFilteredDataByValue(v, minMax.min);
888✔
294
                                const maxData = $$.getFilteredDataByValue(v, minMax.max);
888✔
295

296
                                if (minData.length) {
888✔
297
                                        min = min.concat(minData);
345✔
298
                                }
299

300
                                if (maxData.length) {
888✔
301
                                        max = max.concat(maxData);
333✔
302
                                }
303
                        });
304

305
                        // update the cached data
306
                        $$.cache.add(cacheKey, minMaxData = {min, max});
333✔
307
                }
308

309
                return minMaxData;
2,634✔
310
        },
311

312
        /**
313
         * Get sum of data per index
314
         * @private
315
         * @returns {Array}
316
         */
317
        getTotalPerIndex() {
7,746✔
318
                const $$ = this;
7,746✔
319
                const cacheKey = KEY.dataTotalPerIndex;
7,746✔
320
                let sum = $$.cache.get(cacheKey);
7,746✔
321

322
                if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
7,746!
323
                        sum = [];
7,746✔
324

325
                        $$.data.targets.forEach(row => {
160,176✔
326
                                row.values.forEach((v, i) => {
150,012✔
327
                                        if (!sum[i]) {
150,012✔
328
                                                sum[i] = 0;
33,009✔
329
                                        }
330

331
                                        sum[i] += isNumber(v.value) ? v.value : 0;
150,012✔
332
                                });
333
                        });
334
                }
335

336
                return sum;
7,746✔
337
        },
338

339
        /**
340
         * Get total data sum
341
         * @param {boolean} subtractHidden Subtract hidden data from total
342
         * @returns {number}
343
         * @private
344
         */
345
        getTotalDataSum(subtractHidden) {
1,854✔
346
                const $$ = this;
1,854✔
347
                const cacheKey = KEY.dataTotalSum;
1,854✔
348
                let total = $$.cache.get(cacheKey);
1,854✔
349

350
                if (!isNumber(total)) {
1,854✔
351
                        const sum = mergeArray($$.data.targets.map(t => t.values))
831✔
352
                                .map(v => v.value);
945✔
353

354
                        total = sum.length ? sum.reduce((p, c) => p + c) : 0;
708✔
355

356
                        $$.cache.add(cacheKey, total);
243✔
357
                }
358

359
                if (subtractHidden) {
1,854✔
360
                        total -= $$.getHiddenTotalDataSum();
1,413✔
361
                }
362

363
                return total;
1,854✔
364
        },
365

366
        /**
367
         * Get total hidden data sum
368
         * @returns {number}
369
         * @private
370
         */
371
        getHiddenTotalDataSum() {
1,413✔
372
                const $$ = this;
1,413✔
373
                const {api, state: {hiddenTargetIds}} = $$;
1,413✔
374
                let total = 0;
1,413✔
375

376
                if (hiddenTargetIds.length) {
1,413✔
377
                        total = api.data.values.bind(api)(hiddenTargetIds)
66✔
378
                                .reduce((p, c) => p + c);
15✔
379
                }
380

381
                return total;
1,413✔
382
        },
383

384
        /**
385
         * Get filtered data by value
386
         * @param {object} data Data
387
         * @param {number} value Value to be filtered
388
         * @returns {Array} filtered array data
389
         * @private
390
         */
391
        getFilteredDataByValue(data, value) {
1,776✔
392
                return data.filter(t => this.getBaseValue(t) === value);
4,314✔
393
        },
394

395
        /**
396
         * Return the max length of the data
397
         * @returns {number} max data length
398
         * @private
399
         */
400
        getMaxDataCount(): number {
10,917✔
401
                return Math.max(...this.data.targets.map(t => t.values.length), 0);
29,043✔
402
        },
403

404
        getMaxDataCountTarget() {
6,126✔
405
                let target = this.filterTargetsToShow() || [];
6,126!
406
                const length = target.length;
6,126✔
407
                const isInverted = this.config.axis_x_inverted;
6,126✔
408

409
                if (length > 1) {
6,126✔
410
                        target = target.map(t => t.values)
11,217✔
411
                                .reduce((a, b) => a.concat(b))
7,599✔
412
                                .map(v => v.x);
46,959✔
413

414
                        target = sortValue(getUnique(target))
3,618✔
415
                                .map((x, index, array) => ({x, index: isInverted ? array.length - index - 1 : index}));
18,555!
416
                } else if (length) {
2,508✔
417
                        target = target[0].values.concat();
2,358✔
418
                }
419

420
                return target;
6,126✔
421
        },
422

423
        mapToIds(targets): string[] {
97,899✔
424
                return targets.map(d => d.id);
207,558✔
425
        },
426

427
        mapToTargetIds(ids) {
428
                const $$ = this;
4,752✔
429

430
                return ids ? (isArray(ids) ? ids.concat() : [ids]) : $$.mapToIds($$.data.targets);
4,752✔
431
        },
432

433
        hasTarget(targets, id): boolean {
434
                const ids = this.mapToIds(targets);
63✔
435

436
                for (let i = 0, val; (val = ids[i]); i++) {
63✔
437
                        if (val === id) {
135✔
438
                                return true;
63✔
439
                        }
440
                }
441

UNCOV
442
                return false;
×
443
        },
444

445
        isTargetToShow(targetId): boolean {
446
                return this.state.hiddenTargetIds.indexOf(targetId) < 0;
408,935✔
447
        },
448

449
        isLegendToShow(targetId): boolean {
450
                return this.state.hiddenLegendIds.indexOf(targetId) < 0;
33,963✔
451
        },
452

453
        filterTargetsToShow(targets) {
141,361✔
454
                const $$ = this;
141,361✔
455

456
                return (targets || $$.data.targets).filter(t => $$.isTargetToShow(t.id));
378,797✔
457
        },
458

459
        mapTargetsToUniqueXs(targets) {
20,844✔
460
                const $$ = this;
20,844✔
461
                const {axis} = $$;
20,844✔
462
                let xs: any[] = [];
20,844✔
463

464
                if (targets?.length) {
20,844✔
465
                        xs = getUnique(
20,760✔
466
                                mergeArray(targets.map(t => t.values.map(v => +v.x)))
268,236✔
467
                        );
468

469
                        xs = axis?.isTimeSeries() ? xs.map(x => new Date(+x)) : xs.map(Number);
90,228✔
470
                }
471

472
                return sortValue(xs);
20,844✔
473
        },
474

475
        /**
476
         * Add to the state target Ids
477
         * @param {string} type State's prop name
478
         * @param {Array|string} targetIds Target ids array
479
         * @private
480
         */
481
        addTargetIds(type: string, targetIds: string[] | string): void {
228✔
482
                const {state} = this;
228✔
483
                const ids = (isArray(targetIds) ? targetIds : [targetIds]) as [];
228✔
484

485
                ids.forEach(v => {
327✔
486
                        state[type].indexOf(v) < 0 &&
327✔
487
                                state[type].push(v);
488
                });
489
        },
490

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

501
                ids.forEach(v => {
396✔
502
                        const index = state[type].indexOf(v);
396✔
503

504
                        index >= 0 && state[type].splice(index, 1);
396✔
505
                });
506
        },
507

508
        addHiddenTargetIds(targetIds: string[]): void {
509
                this.addTargetIds("hiddenTargetIds", targetIds);
207✔
510
        },
511

512
        removeHiddenTargetIds(targetIds: string[]): void {
513
                this.removeTargetIds("hiddenTargetIds", targetIds);
69✔
514
        },
515

516
        addHiddenLegendIds(targetIds: string[]): void {
517
                this.addTargetIds("hiddenLegendIds", targetIds);
21✔
518
        },
519

520
        removeHiddenLegendIds(targetIds: string[]): void {
521
                this.removeTargetIds("hiddenLegendIds", targetIds);
114✔
522
        },
523

524
        getValuesAsIdKeyed(targets) {
78,882✔
525
                const $$ = this;
78,882✔
526
                const {hasAxis} = $$.state;
78,882✔
527
                const ys = {};
78,882✔
528
                const isMultipleX = $$.isMultipleX();
78,882✔
529
                const xs = isMultipleX ? $$.mapTargetsToUniqueXs(targets)
78,882✔
530
                        .map(v => (isString(v) ? v : +v)) : null;
17,082!
531

532
                targets.forEach(t => {
331,032✔
533
                        const data: any[] = [];
165,516✔
534

535
                        t.values
165,516✔
536
                                .filter(({value}) => isValue(value) || value === null)
953,790✔
537
                                .forEach(v => {
953,790✔
538
                                        let {value} = v;
953,790✔
539

540
                                        // exclude 'volume' value to correct mis domain calculation
541
                                        if (value !== null && $$.isCandlestickType(v)) {
953,790✔
542
                                                value = isArray(value) ? value.slice(0, 4) : [value.open, value.high, value.low, value.close];
1,308✔
543
                                        }
544

545
                                        if (isArray(value)) {
953,790✔
546
                                                data.push(...value);
6,450✔
547
                                        } else if (isObject(value) && "high" in value) {
947,340✔
548
                                                data.push(...Object.values(value));
1,170✔
549
                                        } else if ($$.isBubbleZType(v)) {
946,170✔
550
                                                data.push(hasAxis && $$.getBubbleZData(value, "y"));
648✔
551
                                        } else {
552
                                                if (isMultipleX) {
945,522✔
553
                                                        data[$$.getIndexByX(v.x, xs)] = value;
25,128✔
554
                                                } else {
555
                                                        data.push(value);
920,394✔
556
                                                }
557
                                        }
558
                                });
559

560
                        ys[t.id] = data;
165,516✔
561
                });
562

563
                return ys;
78,882✔
564
        },
565

566
        checkValueInTargets(targets, checker: Function): boolean {
567
                const ids = Object.keys(targets);
16,932✔
568
                let values;
569

570
                for (let i = 0; i < ids.length; i++) {
16,932✔
571
                        values = targets[ids[i]].values;
41,451✔
572

573
                        for (let j = 0; j < values.length; j++) {
41,451✔
574
                                if (checker(values[j].value)) {
104,037✔
575
                                        return true;
9,675✔
576
                                }
577
                        }
578
                }
579

580
                return false;
7,257✔
581
        },
582

583
        hasMultiTargets(): boolean {
584
                return this.filterTargetsToShow().length > 1;
207✔
585
        },
586

587
        hasNegativeValueInTargets(targets): boolean {
8,466✔
588
                return this.checkValueInTargets(targets, v => v < 0);
91,956✔
589
        },
590

591
        hasPositiveValueInTargets(targets): boolean {
8,466✔
592
                return this.checkValueInTargets(targets, v => v > 0);
12,081✔
593
        },
594

595
        /**
596
         * Sort targets data
597
         * Note: For stacked bar, will sort from the total sum of data series, not for each stacked bar
598
         * @param {Array} targetsValue Target value
599
         * @returns {Array}
600
         * @private
601
         */
602
        orderTargets(targetsValue: IData[]): IData[] {
603
                const $$ = this;
23,982✔
604
                const targets = [...targetsValue];
23,982✔
605
                const fn = $$.getSortCompareFn();
23,982✔
606

607
                fn && targets.sort(fn);
23,982✔
608

609
                return targets;
23,982✔
610
        },
611

612
        /**
613
         * Get data.order compare function
614
         * @param {boolean} isReversed for Arc & Treemap type sort order needs to be reversed
615
         * @returns {Function} compare function
616
         * @private
617
         */
618
        getSortCompareFn(isReversed = false): Function | null {
24,585✔
619
                const $$ = this;
24,585✔
620
                const {config} = $$;
24,585✔
621
                const order = config.data_order;
24,585✔
622
                const orderAsc = /asc/i.test(order);
24,585✔
623
                const orderDesc = /desc/i.test(order);
24,585✔
624
                let fn;
625

626
                if (orderAsc || orderDesc) {
24,585✔
627
                        const reducer = (p, c) => p + Math.abs(c.value);
346,649✔
628

629
                        fn = (t1: IData | IDataRow, t2: IData | IDataRow) => {
60,082✔
630
                                const t1Sum = "values" in t1 ? t1.values.reduce(reducer, 0) : t1.value;
60,082✔
631
                                const t2Sum = "values" in t2 ? t2.values.reduce(reducer, 0) : t2.value;
60,082✔
632

633
                                return isReversed ?
60,082✔
634
                                        (orderAsc ? t1Sum - t2Sum : t2Sum - t1Sum) :
78,995!
635
                                        (orderAsc ? t2Sum - t1Sum : t1Sum - t2Sum);
41,169✔
636
                        };
637
                } else if (isFunction(order)) {
747✔
638
                        fn = order.bind($$.api);
54✔
639
                }
640

641
                return fn || null;
24,585✔
642
        },
643

644
        filterByX(targets, x) {
213✔
645
                return mergeArray(targets.map(t => t.values)).filter(v => v.x - x === 0);
2,091✔
646
        },
647

648
        filterRemoveNull(data) {
66✔
649
                return data.filter(d => isValue(this.getBaseValue(d)));
384✔
650
        },
651

652
        filterByXDomain(targets, xDomain) {
63✔
653
                return targets.map(t => ({
234✔
654
                        id: t.id,
655
                        id_org: t.id_org,
656
                        values: t.values.filter(v => xDomain[0] <= v.x && v.x <= xDomain[1])
1,554✔
657
                }));
658
        },
659

660
        hasDataLabel() {
661
                const dataLabels = this.config.data_labels;
86,199✔
662

663
                return (isboolean(dataLabels) && dataLabels) ||
86,199✔
664
                        (isObjectType(dataLabels) && notEmpty(dataLabels));
665
        },
666

667
        /**
668
         * Get data index from the event coodinates
669
         * @param {Event} event Event object
670
         * @returns {number}
671
         */
672
        getDataIndexFromEvent(event): number {
673
                const $$ = this;
1,347✔
674
                const {config, state: {hasRadar, inputType, eventReceiver: {coords, rect}}} = $$;
1,347✔
675
                let index;
676

677
                if (hasRadar) {
1,347✔
678
                        let target = event.target;
18✔
679

680
                        // in case of multilined axis text
681
                        if (/tspan/i.test(target.tagName)) {
18!
UNCOV
682
                                target = target.parentNode;
×
683
                        }
684

685
                        const d: any = d3Select(target).datum();
18✔
686

687
                        index = d && Object.keys(d).length === 1 ? d.index : undefined;
18!
688
                } else {
689
                        const isRotated = config.axis_rotated;
1,329✔
690

691
                        // get data based on the mouse coords
692
                        const e = inputType === "touch" && event.changedTouches ? event.changedTouches[0] : event;
1,329✔
693

694
                        index = findIndex(
1,329✔
695
                                coords,
696
                                isRotated ? e.clientY - rect.top : e.clientX - rect.left,
1,329✔
697
                                0,
698
                                coords.length - 1,
699
                                isRotated
700
                        );
701
                }
702

703
                return index;
1,347✔
704
        },
705

706
        getDataLabelLength(min, max, key) {
3,078✔
707
                const $$ = this;
3,078✔
708
                const lengths = [0, 0];
3,078✔
709
                const paddingCoef = 1.3;
710

711
                $$.$el.chart.select("svg").selectAll(".dummy")
3,078✔
712
                        .data([min, max])
713
                        .enter()
714
                        .append("text")
715
                        .text(d => $$.dataLabelFormat(d.id)(d))
5,136✔
716
                        .each(function(d, i) {
717
                                lengths[i] = this.getBoundingClientRect()[key] * paddingCoef;
5,136✔
718
                        })
719
                        .remove();
720

721
                return lengths;
3,078✔
722
        },
723

724
        isNoneArc(d) {
UNCOV
725
                return this.hasTarget(this.data.targets, d.id);
×
726
        },
727

728
        isArc(d) {
UNCOV
729
                return "data" in d && this.hasTarget(this.data.targets, d.data.id);
×
730
        },
731

732
        findSameXOfValues(values, index) {
UNCOV
733
                const targetX = values[index].x;
×
UNCOV
734
                const sames: any[] = [];
×
735
                let i;
736

UNCOV
737
                for (i = index - 1; i >= 0; i--) {
×
UNCOV
738
                        if (targetX !== values[i].x) {
×
739
                                break;
×
740
                        }
741

UNCOV
742
                        sames.push(values[i]);
×
743
                }
744

745
                for (i = index; i < values.length; i++) {
×
UNCOV
746
                        if (targetX !== values[i].x) {
×
UNCOV
747
                                break;
×
748
                        }
749

UNCOV
750
                        sames.push(values[i]);
×
751
                }
752

753
                return sames;
×
754
        },
755

756
        findClosestFromTargets(targets, pos: [number, number]): IDataRow | undefined {
90✔
757
                const $$ = this;
90✔
758
                const candidates = targets.map(target => $$.findClosest(target.values, pos)); // map to array of closest points of each target
168✔
759

760
                // decide closest point and return
761
                return $$.findClosest(candidates, pos);
90✔
762
        },
763

764
        findClosest(values, pos: [number, number]): IDataRow | undefined {
240✔
765
                const $$ = this;
240✔
766
                const {$el: {main}} = $$;
240✔
767
                const data = values.filter(v => v && isValue(v.value));
888✔
768

769
                let minDist;
770
                let closest;
771

772
                // find mouseovering bar/candlestick
773
                // https://github.com/naver/billboard.js/issues/2434
774
                data
240✔
775
                        .filter(v => $$.isBarType(v.id) || $$.isCandlestickType(v.id))
804✔
776
                        .forEach(v => {
30✔
777
                                const selector = $$.isBarType(v.id) ?
30✔
778
                                        `.${$BAR.chartBar}.${$COMMON.target}${$$.getTargetSelectorSuffix(v.id)} .${$BAR.bar}-${v.index}` :
779
                                        `.${$CANDLESTICK.chartCandlestick}.${$COMMON.target}${$$.getTargetSelectorSuffix(v.id)} .${$CANDLESTICK.candlestick}-${v.index} path`;
780

781
                                if (!closest && $$.isWithinBar(main.select(selector).node())) {
30✔
782
                                        closest = v;
6✔
783
                                }
784
                        });
785

786
                // find closest point from non-bar/candlestick
787
                data
240✔
788
                        .filter(v => !$$.isBarType(v.id) && !$$.isCandlestickType(v.id))
804✔
789
                        .forEach((v: IDataPoint) => {
774✔
790
                                const d = $$.dist(v, pos);
774✔
791

792
                                minDist = $$.getPointSensitivity(v);
774✔
793

794
                                if (d < minDist) {
774✔
795
                                        minDist = d;
168✔
796
                                        closest = v;
168✔
797
                                }
798
                        });
799

800
                return closest;
240✔
801
        },
802

803
        dist(data: IDataPoint, pos: [number, number]) {
804
                const $$ = this;
876✔
805
                const {config: {axis_rotated: isRotated}, scale} = $$;
876✔
806
                const xIndex = +isRotated; // true: 1, false: 0
807
                const yIndex = +!isRotated; // true: 0, false: 1
808
                const y = $$.circleY(data, data.index);
876✔
809
                const x = (scale.zoom || scale.x)(data.x);
876✔
810

811
                return Math.sqrt(Math.pow(x - pos[xIndex], 2) + Math.pow(y - pos[yIndex], 2));
876✔
812
        },
813

814
        /**
815
         * Convert data for step type
816
         * @param {Array} values Object data values
817
         * @returns {Array}
818
         * @private
819
         */
820
        convertValuesToStep(values) {
821
                const $$ = this;
2,160✔
822
                const {axis, config} = $$;
2,160✔
823
                const stepType = config.line_step_type;
2,160✔
824
                const isCategorized = axis ? axis.isCategorized() : false;
2,160!
825
                const converted = isArray(values) ? values.concat() : [values];
2,160!
826

827
                if (!(isCategorized || /step\-(after|before)/.test(stepType))) {
2,160✔
828
                        return values;
984✔
829
                }
830

831
                // when all datas are null, return empty array
832
                // https://github.com/naver/billboard.js/issues/3124
833
                if (converted.length) {
1,176✔
834
                        // insert & append cloning first/last value to be fully rendered covering on each gap sides
835
                        const head = converted[0];
1,170✔
836
                        const tail = converted[converted.length - 1];
1,170✔
837
                        const {id} = head;
1,170✔
838
                        let {x} = head;
1,170✔
839

840
                        // insert head
841
                        converted.unshift({x: --x, value: head.value, id});
1,170✔
842

843
                        isCategorized && stepType === "step-after" &&
1,170✔
844
                                converted.unshift({x: --x, value: head.value, id});
845

846
                        // append tail
847
                        x = tail.x;
1,170✔
848
                        converted.push({x: ++x, value: tail.value, id});
1,170✔
849

850
                        isCategorized && stepType === "step-before" &&
1,170✔
851
                                converted.push({x: ++x, value: tail.value, id});
852
                }
853

854
                return converted;
1,176✔
855
        },
856

UNCOV
857
        convertValuesToRange(values) {
×
UNCOV
858
                const converted = isArray(values) ? values.concat() : [values];
×
UNCOV
859
                const ranges: {x: string | number, id: string, value: number}[] = [];
×
860

UNCOV
861
                converted.forEach(range => {
×
UNCOV
862
                        const {x, id} = range;
×
863

864
                        ranges.push({
×
865
                                x,
866
                                id,
867
                                value: range.value[0]
868
                        });
869

870
                        ranges.push({
×
871
                                x,
872
                                id,
873
                                value: range.value[2]
874
                        });
875
                });
876

UNCOV
877
                return ranges;
×
878
        },
879

880
        updateDataAttributes(name, attrs) {
18✔
881
                const $$ = this;
18✔
882
                const {config} = $$;
18✔
883
                const current = config[`data_${name}`];
18✔
884

885
                if (isUndefined(attrs)) {
18✔
886
                        return current;
9✔
887
                }
888

889
                Object.keys(attrs).forEach(id => {
18✔
890
                        current[id] = attrs[id];
18✔
891
                });
892

893
                $$.redraw({withLegend: true});
9✔
894

895
                return current;
9✔
896
        },
897

898
        getRangedData(d, key = "", type = "areaRange"): number | undefined {
3,252!
899
                const value = d?.value;
3,252!
900

901
                if (isArray(value)) {
3,252✔
902
                        // @ts-ignore
903
                        const index = {
2,406✔
904
                                areaRange: ["high", "mid", "low"],
905
                                candlestick: ["open", "high", "low", "close", "volume"]
906
                        }[type].indexOf(key);
907

908
                        return index >= 0 && value ? value[index] : undefined;
2,406!
909
                } else if (value) {
846!
910
                        return value[key];
846✔
911
                }
912

UNCOV
913
                return value;
×
914
        },
915

916
        /**
917
         * Set ratio for grouped data
918
         * @param {Array} data Data array
919
         * @private
920
         */
921
        setRatioForGroupedData(data: (IDataRow | IData)[]): void {
2,457✔
922
                const $$ = this;
2,457✔
923
                const {config} = $$;
2,457✔
924

925
                // calculate ratio if grouped data exists
926
                if (config.data_groups.length && data.some(d => $$.isGrouped(d.id))) {
2,457✔
927
                        const setter = (d: IDataRow) => $$.getRatio("index", d, true);
6,666✔
928

929
                        data.forEach(v => {
6,039✔
930
                                "values" in v ? v.values.forEach(setter) : setter(v);
6,039✔
931
                        });
932
                }
933
        },
934

935
        /**
936
         * Get ratio value
937
         * @param {string} type Ratio for given type
938
         * @param {object} d Data value object
939
         * @param {boolean} asPercent Convert the return as percent or not
940
         * @returns {number} Ratio value
941
         * @private
942
         */
943
        getRatio(type: string, d, asPercent = false): number {
13,152✔
944
                const $$ = this;
13,152✔
945
                const {config, state} = $$;
13,152✔
946
                const api = $$.api;
13,152✔
947
                let ratio = 0;
13,152✔
948

949
                if (d && api.data.shown().length) {
13,152✔
950
                        ratio = d.ratio || d.value;
13,107✔
951

952
                        if (type === "arc") {
13,107✔
953
                                // if has padAngle set, calculate rate based on value
954
                                if ($$.pie.padAngle()()) {
1,845✔
955
                                        ratio = d.value / $$.getTotalDataSum(true);
54✔
956

957
                                        // otherwise, based on the rendered angle value
958
                                } else {
959
                                        const gaugeArcLength = config.gauge_fullCircle ?
1,791✔
960
                                                $$.getArcLength() : $$.getGaugeStartAngle() * -2;
1,791✔
961
                                        const arcLength = $$.hasType("gauge") ? gaugeArcLength : Math.PI * 2;
1,791✔
962

963
                                        ratio = (d.endAngle - d.startAngle) / arcLength;
1,791✔
964
                                }
965
                        } else if (type === "index") {
11,262✔
966
                                const dataValues = api.data.values.bind(api);
7,746✔
967
                                let total = this.getTotalPerIndex();
7,746✔
968

969
                                if (state.hiddenTargetIds.length) {
7,746✔
970
                                        let hiddenSum = dataValues(state.hiddenTargetIds, false);
1,026✔
971

972
                                        if (hiddenSum.length) {
1,026✔
973
                                                hiddenSum = hiddenSum
258✔
974
                                                        .reduce((acc, curr) => acc.map((v, i) => (isNumber(v) ? v : 0) + curr[i]));
288!
975

976
                                                total = total.map((v, i) => v - hiddenSum[i]);
630✔
977
                                        }
978
                                }
979

980
                                const divisor = total[d.index];
7,746✔
981

982
                                d.ratio = isNumber(d.value) && total && divisor ?
7,746✔
983
                                        d.value / divisor : 0;
7,746✔
984

985
                                ratio = d.ratio;
7,746✔
986
                        } else if (type === "radar") {
3,516✔
987
                                ratio = (
1,191✔
988
                                        parseFloat(String(Math.max(d.value, 0))) / state.current.dataMax
989
                                ) * config.radar_size_ratio;
990
                        } else if (type === "bar") {
2,325✔
991
                                const yScale = $$.getYScaleById.bind($$)(d.id);
1,449✔
992
                                const max = yScale.domain().reduce((a, c) => c - a);
1,449✔
993

994
                                // when all data are 0, return 0
995
                                ratio = max === 0 ? 0 : Math.abs(d.value) / max;
1,449✔
996
                        } else if (type === "treemap") {
876!
997
                                ratio /= $$.getTotalDataSum(true);
876✔
998
                        }
999
                }
1000

1001
                return asPercent && ratio ? ratio * 100 : ratio;
13,152✔
1002
        },
1003

1004
        /**
1005
         * Sort data index to be aligned with x axis.
1006
         * @param {Array} tickValues Tick array values
1007
         * @private
1008
         */
1009
        updateDataIndexByX(tickValues) {
6,126✔
1010
                const $$ = this;
6,126✔
1011

1012
                const tickValueMap = tickValues.reduce((out, tick, index) => {
41,106✔
1013
                        out[Number(tick.x)] = index;
41,106✔
1014
                        return out;
41,106✔
1015
                }, {});
1016

1017
                $$.data.targets.forEach(t => {
27,660✔
1018
                        t.values.forEach((value, valueIndex) => {
71,133✔
1019
                                let index = tickValueMap[Number(value.x)];
71,133✔
1020

1021
                                if (index === undefined) {
71,133✔
1022
                                        index = valueIndex;
1,173✔
1023
                                }
1024
                                value.index = index;
71,133✔
1025
                        });
1026
                });
1027
        },
1028

1029
        /**
1030
         * Determine if bubble has dimension data
1031
         * @param {object|Array} d data value
1032
         * @returns {boolean}
1033
         * @private
1034
         */
1035
        isBubbleZType(d): boolean {
1036
                const $$ = this;
1,276,644✔
1037

1038
                return $$.isBubbleType(d) && (
1,276,644!
1039
                        (isObject(d.value) && ("z" in d.value || "y" in d.value)) ||
1040
                        (isArray(d.value) && d.value.length >= 2)
1041
                );
1042
        },
1043

1044
        /**
1045
         * Determine if bar has ranged data
1046
         * @param {Array} d data value
1047
         * @returns {boolean}
1048
         * @private
1049
         */
1050
        isBarRangeType(d): boolean {
78,780✔
1051
                const $$ = this;
78,780✔
1052
                const {value} = d;
78,780✔
1053

1054
                return $$.isBarType(d) && isArray(value) && value.length >= 2 && value.every(v => isNumber(v));
78,780✔
1055
        },
1056

1057
        /**
1058
         * Get data object by id
1059
         * @param {string} id data id
1060
         * @returns {object}
1061
         * @private
1062
         */
1063
        getDataById(id: string) {
1064
                const d = this.cache.get(id) || this.api.data(id);
24,219✔
1065

1066
                return d?.[0] ?? d;
24,219!
1067
        }
1068
};
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

© 2025 Coveralls, Inc