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

naver / billboard.js / 16137267914

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

push

github

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

update dependencies to the latest versions

5668 of 7016 branches covered (80.79%)

Branch coverage included in aggregate %.

7487 of 8115 relevant lines covered (92.26%)

11761.7 hits per line

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

83.55
/src/module/util.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 * @ignore
5
 */
6
import {brushSelection as d3BrushSelection} from "d3-brush";
7
import {pointer as d3Pointer} from "d3-selection";
8
import type {d3Selection} from "../../types/types";
9
import {document, requestAnimationFrame, window} from "./browser";
10

11
export {
12
        addCssRules,
13
        asHalfPixel,
14
        brushEmpty,
15
        callFn,
16
        camelize,
17
        capitalize,
18
        ceil10,
19
        convertInputType,
20
        deepClone,
21
        diffDomain,
22
        emulateEvent,
23
        endall,
24
        extend,
25
        findIndex,
26
        getBBox,
27
        getBoundingRect,
28
        getBrushSelection,
29
        getCssRules,
30
        getMinMax,
31
        getOption,
32
        getPathBox,
33
        getPointer,
34
        getRandom,
35
        getRange,
36
        getRectSegList,
37
        getScrollPosition,
38
        getTransformCTM,
39
        getTranslation,
40
        getUnique,
41
        hasStyle,
42
        hasValue,
43
        hasViewBox,
44
        isArray,
45
        isBoolean,
46
        isDefined,
47
        isEmpty,
48
        isFunction,
49
        isNumber,
50
        isObject,
51
        isObjectType,
52
        isString,
53
        isTabVisible,
54
        isUndefined,
55
        isValue,
56
        mergeArray,
57
        mergeObj,
58
        notEmpty,
59
        parseDate,
60
        runUntil,
61
        sanitize,
62
        setTextValue,
63
        sortValue,
64
        toArray,
65
        tplProcess
66
};
67

68
const isValue = (v: any): boolean => v || v === 0;
837,819✔
69
const isFunction = (v: unknown): v is (...args: any[]) => any => typeof v === "function";
684,259✔
70
const isString = (v: unknown): v is string => typeof v === "string";
2,531,803✔
71
const isNumber = (v: unknown): v is number => typeof v === "number";
1,379,845✔
72
const isUndefined = (v: unknown): v is undefined => typeof v === "undefined";
1,123,179✔
73
const isDefined = (v: unknown): boolean => typeof v !== "undefined";
1,553,763✔
74
const isBoolean = (v: unknown): boolean => typeof v === "boolean";
38,187✔
75
const ceil10 = (v: number): number => Math.ceil(v / 10) * 10;
23,805✔
76
const asHalfPixel = (n: number): number => Math.ceil(n) + 0.5;
24,696✔
77
const diffDomain = (d: number[]): number => d[1] - d[0];
11,718✔
78
const isObjectType = (v: unknown): v is Record<string | number, any> => typeof v === "object";
3,355,260✔
79
const isEmpty = (o: unknown): boolean => (
231✔
80
        isUndefined(o) || o === null ||
963,519✔
81
        (isString(o) && o.length === 0) ||
82
        (isObjectType(o) && !(o instanceof Date) && Object.keys(o).length === 0) ||
83
        (isNumber(o) && isNaN(o))
84
);
85
const notEmpty = (o: unknown): boolean => !isEmpty(o);
957,684✔
86

87
/**
88
 * Check if is array
89
 * @param {Array} arr Data to be checked
90
 * @returns {boolean}
91
 * @private
92
 */
93
const isArray = (arr: any): arr is any[] => Array.isArray(arr);
2,189,422✔
94

95
/**
96
 * Check if is object
97
 * @param {object} obj Data to be checked
98
 * @returns {boolean}
99
 * @private
100
 */
101
const isObject = (obj: any): boolean => obj && !obj?.nodeType && isObjectType(obj) && !isArray(obj);
1,723,839✔
102

103
/**
104
 * Get specified key value from object
105
 * If default value is given, will return if given key value not found
106
 * @param {object} options Source object
107
 * @param {string} key Key value
108
 * @param {*} defaultValue Default value
109
 * @returns {*}
110
 * @private
111
 */
112
function getOption(options: object, key: string, defaultValue): any {
113
        return isDefined(options[key]) ? options[key] : defaultValue;
60,042✔
114
}
115

116
/**
117
 * Check if value exist in the given object
118
 * @param {object} dict Target object to be checked
119
 * @param {*} value Value to be checked
120
 * @returns {boolean}
121
 * @private
122
 */
123
function hasValue(dict: object, value: any): boolean {
124
        let found = false;
321✔
125

126
        Object.keys(dict).forEach(key => (dict[key] === value) && (found = true));
1,053✔
127

128
        return found;
321✔
129
}
130

131
/**
132
 * Call function with arguments
133
 * @param {Function} fn Function to be called
134
 * @param {*} thisArg "this" value for fn
135
 * @param {*} args Arguments for fn
136
 * @returns {boolean} true: fn is function, false: fn is not function
137
 * @private
138
 */
139
function callFn(fn: unknown, thisArg: any, ...args: any[]): boolean {
140
        const isFn = isFunction(fn);
14,410✔
141

142
        isFn && fn.call(thisArg, ...args);
14,410✔
143
        return isFn;
14,410✔
144
}
145

146
/**
147
 * Call function after all transitions ends
148
 * @param {d3.transition} transition Transition
149
 * @param {Fucntion} cb Callback function
150
 * @private
151
 */
152
function endall(transition, cb: Function): void {
153
        let n = 0;
930✔
154

155
        const end = function(...args) {
930✔
156
                !--n && cb.apply(this, ...args);
2,161✔
157
        };
158

159
        // if is transition selection
160
        if ("duration" in transition) {
930✔
161
                transition
885✔
162
                        .each(() => ++n)
2,280✔
163
                        .on("end", end);
164
        } else {
165
                ++n;
45✔
166
                transition.call(end);
45✔
167
        }
168
}
169

170
/**
171
 * Replace tag sign to html entity
172
 * @param {string} str Target string value
173
 * @returns {string}
174
 * @private
175
 */
176
function sanitize(str: string): string {
177
        return isString(str) ?
2,070✔
178
                str.replace(/<(script|img)?/ig, "&lt;").replace(/(script)?>/ig, "&gt;") :
179
                str;
180
}
181

182
/**
183
 * Set text value. If there's multiline add nodes.
184
 * @param {d3Selection} node Text node
185
 * @param {string} text Text value string
186
 * @param {Array} dy dy value for multilined text
187
 * @param {boolean} toMiddle To be alingned vertically middle
188
 * @private
189
 */
190
function setTextValue(
191
        node: d3Selection,
192
        text: string,
193
        dy: number[] = [-1, 1],
462✔
194
        toMiddle: boolean = false
1,068✔
195
) {
196
        if (!node || !isString(text)) {
2,886!
197
                return;
×
198
        }
199

200
        if (text.indexOf("\n") === -1) {
2,886✔
201
                node.text(text);
2,367✔
202
        } else {
203
                const diff = [node.text(), text].map(v => v.replace(/[\s\n]/g, ""));
1,038✔
204

205
                if (diff[0] !== diff[1]) {
519✔
206
                        const multiline = text.split("\n");
501✔
207
                        const len = toMiddle ? multiline.length - 1 : 1;
501✔
208

209
                        // reset possible text
210
                        node.html("");
501✔
211

212
                        multiline.forEach((v, i) => {
501✔
213
                                node.append("tspan")
1,011✔
214
                                        .attr("x", 0)
215
                                        .attr("dy", `${i === 0 ? dy[0] * len : dy[1]}em`)
1,011✔
216
                                        .text(v);
217
                        });
218
                }
219
        }
220
}
221

222
/**
223
 * Substitution of SVGPathSeg API polyfill
224
 * @param {SVGGraphicsElement} path Target svg element
225
 * @returns {Array}
226
 * @private
227
 */
228
function getRectSegList(path: SVGGraphicsElement): {x: number, y: number}[] {
229
        /*
230
         * seg1 ---------- seg2
231
         *   |               |
232
         *   |               |
233
         *   |               |
234
         * seg0 ---------- seg3
235
         */
236
        const {x, y, width, height} = path.getBBox();
318✔
237

238
        return [
318✔
239
                {x, y: y + height}, // seg0
240
                {x, y}, // seg1
241
                {x: x + width, y}, // seg2
242
                {x: x + width, y: y + height} // seg3
243
        ];
244
}
245

246
/**
247
 * Get svg bounding path box dimension
248
 * @param {SVGGraphicsElement} path Target svg element
249
 * @returns {object}
250
 * @private
251
 */
252
function getPathBox(
253
        path: SVGGraphicsElement
254
): {x: number, y: number, width: number, height: number} {
255
        const {width, height} = getBoundingRect(path);
231✔
256
        const items = getRectSegList(path);
231✔
257
        const x = items[0].x;
231✔
258
        const y = Math.min(items[0].y, items[1].y);
231✔
259

260
        return {
231✔
261
                x,
262
                y,
263
                width,
264
                height
265
        };
266
}
267

268
/**
269
 * Get event's current position coordinates
270
 * @param {object} event Event object
271
 * @param {SVGElement|HTMLElement} element Target element
272
 * @returns {Array} [x, y] Coordinates x, y array
273
 * @private
274
 */
275
function getPointer(event, element?: SVGElement): number[] {
276
        const touches = event &&
1,224✔
277
                (event.touches || (event.sourceEvent && event.sourceEvent.touches))?.[0];
2,457✔
278
        let pointer = [0, 0];
1,224✔
279

280
        try {
1,224✔
281
                pointer = d3Pointer(touches || event, element);
1,224✔
282
        } catch {}
283

284
        return pointer.map(v => (isNaN(v) ? 0 : v));
2,448✔
285
}
286

287
/**
288
 * Return brush selection array
289
 * @param {object} ctx Current instance
290
 * @returns {d3.brushSelection}
291
 * @private
292
 */
293
function getBrushSelection(ctx) {
294
        const {event, $el} = ctx;
843✔
295
        const main = $el.subchart.main || $el.main;
843!
296
        let selection;
297

298
        // check from event
299
        if (event && event.type === "brush") {
843!
300
                selection = event.selection;
×
301
                // check from brush area selection
302
        } else if (main && (selection = main.select(".bb-brush").node())) {
843!
303
                selection = d3BrushSelection(selection);
843✔
304
        }
305

306
        return selection;
843✔
307
}
308

309
/**
310
 * Get boundingClientRect.
311
 * Cache the evaluated value once it was called.
312
 * @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
313
 * @param {SVGElement} node Target element
314
 * @param {boolean} forceEval Force evaluation
315
 * @returns {object}
316
 * @private
317
 */
318
function getRect(
319
        relativeViewport: boolean,
320
        node: SVGElement & Partial<{rect: DOMRect | SVGRect}>,
321
        forceEval = false
×
322
): DOMRect | SVGRect {
323
        const _ = n => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
113,769✔
324

325
        if (forceEval) {
113,769✔
326
                return _(node);
103,242✔
327
        } else {
328
                // will cache the value if the element is not a SVGElement or the width is not set
329
                const needEvaluate = !("rect" in node) || (
10,527✔
330
                        "rect" in node && node.hasAttribute("width") &&
331
                        node.rect!.width !== +(node.getAttribute("width") || 0)
378!
332
                );
333

334
                return needEvaluate ? (node.rect = _(node)) : node.rect!;
10,527✔
335
        }
336
}
337

338
/**
339
 * Get boundingClientRect.
340
 * @param {SVGElement} node Target element
341
 * @param {boolean} forceEval Force evaluation
342
 * @returns {object}
343
 * @private
344
 */
345
function getBoundingRect(node, forceEval = false) {
9,546✔
346
        return getRect(true, node, forceEval);
113,325✔
347
}
348

349
/**
350
 * Get BBox.
351
 * @param {SVGElement} node Target element
352
 * @param {boolean} forceEval Force evaluation
353
 * @returns {object}
354
 * @private
355
 */
356
function getBBox(node, forceEval = false) {
108✔
357
        return getRect(false, node, forceEval);
444✔
358
}
359

360
/**
361
 * Retrun random number
362
 * @param {boolean} asStr Convert returned value as string
363
 * @param {number} min Minimum value
364
 * @param {number} max Maximum value
365
 * @returns {number|string}
366
 * @private
367
 */
368
function getRandom(asStr = true, min = 0, max = 10000) {
50,157✔
369
        const crpt = window.crypto || window.msCrypto;
16,785!
370
        const rand = crpt ?
16,785!
371
                min + crpt.getRandomValues(new Uint32Array(1))[0] % (max - min + 1) :
372
                Math.floor(Math.random() * (max - min) + min);
373

374
        return asStr ? String(rand) : rand;
16,785!
375
}
376

377
/**
378
 * Find index based on binary search
379
 * @param {Array} arr Data array
380
 * @param {number} v Target number to find
381
 * @param {number} start Start index of data array
382
 * @param {number} end End index of data arr
383
 * @param {boolean} isRotated Weather is roted axis
384
 * @returns {number} Index number
385
 * @private
386
 */
387
function findIndex(arr, v: number, start: number, end: number, isRotated: boolean): number {
388
        if (start > end) {
795✔
389
                return -1;
3✔
390
        }
391

392
        const mid = Math.floor((start + end) / 2);
792✔
393
        let {x, w = 0} = arr[mid];
792!
394

395
        if (isRotated) {
792✔
396
                x = arr[mid].y;
108✔
397
                w = arr[mid].h;
108✔
398
        }
399

400
        if (v >= x && v <= x + w) {
792✔
401
                return mid;
411✔
402
        }
403

404
        return v < x ?
381✔
405
                findIndex(arr, v, start, mid - 1, isRotated) :
406
                findIndex(arr, v, mid + 1, end, isRotated);
407
}
408

409
/**
410
 * Check if brush is empty
411
 * @param {object} ctx Bursh context
412
 * @returns {boolean}
413
 * @private
414
 */
415
function brushEmpty(ctx): boolean {
416
        const selection = getBrushSelection(ctx);
663✔
417

418
        if (selection) {
663✔
419
                // brush selected area
420
                // two-dimensional: [[x0, y0], [x1, y1]]
421
                // one-dimensional: [x0, x1] or [y0, y1]
422
                return selection[0] === selection[1];
207✔
423
        }
424

425
        return true;
456✔
426
}
427

428
/**
429
 * Deep copy object
430
 * @param {object} objectN Source object
431
 * @returns {object} Cloned object
432
 * @private
433
 */
434
function deepClone(...objectN) {
435
        const clone = v => {
2,904✔
436
                if (isObject(v) && v.constructor) {
1,132,932✔
437
                        const r = new v.constructor();
108,933✔
438

439
                        for (const k in v) {
108,933✔
440
                                r[k] = clone(v[k]);
1,106,796✔
441
                        }
442

443
                        return r;
108,933✔
444
                }
445

446
                return v;
1,023,999✔
447
        };
448

449
        return objectN.map(v => clone(v))
26,136✔
450
                .reduce((a, c) => (
451
                        {...a, ...c}
23,232✔
452
                ));
453
}
454

455
/**
456
 * Extend target from source object
457
 * @param {object} target Target object
458
 * @param {object|Array} source Source object
459
 * @returns {object}
460
 * @private
461
 */
462
function extend(target = {}, source): object {
×
463
        if (isArray(source)) {
71,778✔
464
                source.forEach(v => extend(target, v));
61,425✔
465
        }
466

467
        // exclude name with only numbers
468
        for (const p in source) {
71,778✔
469
                if (/^\d+$/.test(p) || p in target) {
501,081✔
470
                        continue;
381,276✔
471
                }
472

473
                target[p] = source[p];
119,805✔
474
        }
475

476
        return target;
71,778✔
477
}
478

479
/**
480
 * Return first letter capitalized
481
 * @param {string} str Target string
482
 * @returns {string} capitalized string
483
 * @private
484
 */
485
const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);
93,381✔
486

487
/**
488
 * Camelize from kebob style string
489
 * @param {string} str Target string
490
 * @param {string} separator Separator string
491
 * @returns {string} camelized string
492
 * @private
493
 */
494
function camelize(str: string, separator = "-"): string {
123✔
495
        return str.split(separator)
123✔
496
                .map((v, i) => (
497
                        i ? v.charAt(0).toUpperCase() + v.slice(1).toLowerCase() : v.toLowerCase()
171✔
498
                ))
499
                .join("");
500
}
501

502
/**
503
 * Convert to array
504
 * @param {object} v Target to be converted
505
 * @returns {Array}
506
 * @private
507
 */
508
const toArray = (v: CSSStyleDeclaration | any): any => [].slice.call(v);
6,885✔
509

510
/**
511
 * Add CSS rules
512
 * @param {object} style Style object
513
 * @param {string} selector Selector string
514
 * @param {Array} prop Prps arrary
515
 * @returns {number} Newely added rule index
516
 * @private
517
 */
518
function addCssRules(style, selector: string, prop: string[]): number {
519
        const {rootSelector = "", sheet} = style;
132✔
520
        const getSelector = s =>
132✔
521
                s
132✔
522
                        .replace(/\s?(bb-)/g, ".$1")
523
                        .replace(/\.+/g, ".");
524

525
        const rule = `${rootSelector} ${getSelector(selector)} {${prop.join(";")}}`;
132✔
526

527
        return sheet[sheet.insertRule ? "insertRule" : "addRule"](
132!
528
                rule,
529
                sheet.cssRules.length
530
        );
531
}
532

533
/**
534
 * Get css rules for specified stylesheets
535
 * @param {Array} styleSheets The stylesheets to get the rules from
536
 * @returns {Array}
537
 * @private
538
 */
539
function getCssRules(styleSheets: any[]) {
540
        let rules = [];
30✔
541

542
        styleSheets.forEach(sheet => {
30✔
543
                try {
99✔
544
                        if (sheet.cssRules && sheet.cssRules.length) {
99!
545
                                rules = rules.concat(toArray(sheet.cssRules));
99✔
546
                        }
547
                } catch (e) {
548
                        window.console?.warn(`Error while reading rules from ${sheet.href}: ${e.toString()}`);
×
549
                }
550
        });
551

552
        return rules;
30✔
553
}
554

555
/**
556
 * Get current window and container scroll position
557
 * @param {HTMLElement} node Target element
558
 * @returns {object} window scroll position
559
 * @private
560
 */
561
function getScrollPosition(node: HTMLElement) {
562
        return {
3,777✔
563
                x: (window.pageXOffset ?? window.scrollX ?? 0) + (node.scrollLeft ?? 0),
7,554!
564
                y: (window.pageYOffset ?? window.scrollY ?? 0) + (node.scrollTop ?? 0)
7,554!
565
        };
566
}
567

568
/**
569
 * Get translation string from screen <--> svg point
570
 * @param {SVGGraphicsElement} node graphics element
571
 * @param {number} x target x point
572
 * @param {number} y target y point
573
 * @param {boolean} inverse inverse flag
574
 * @returns {object}
575
 */
576
function getTransformCTM(node: SVGGraphicsElement, x = 0, y = 0, inverse = true): DOMPoint {
×
577
        const point = new DOMPoint(x, y);
×
578
        const screen = <DOMMatrix>node.getScreenCTM();
×
579
        const res = point.matrixTransform(
×
580
                inverse ? screen?.inverse() : screen
×
581
        );
582

583
        if (inverse === false) {
×
584
                const rect = getBoundingRect(node);
×
585

586
                res.x -= rect.x;
×
587
                res.y -= rect.y;
×
588
        }
589

590
        return res;
×
591
}
592

593
/**
594
 * Gets the SVGMatrix of an SVGGElement
595
 * @param {SVGElement} node Node element
596
 * @returns {SVGMatrix} matrix
597
 * @private
598
 */
599
function getTranslation(node) {
600
        const transform = node ? node.transform : null;
30!
601
        const baseVal = transform && transform.baseVal;
30✔
602

603
        return baseVal && baseVal.numberOfItems ?
30!
604
                baseVal.getItem(0).matrix :
605
                {a: 0, b: 0, c: 0, d: 0, e: 0, f: 0};
606
}
607

608
/**
609
 * Get unique value from array
610
 * @param {Array} data Source data
611
 * @returns {Array} Unique array value
612
 * @private
613
 */
614
function getUnique(data: any[]): any[] {
615
        const isDate = data[0] instanceof Date;
12,750✔
616
        const d = (isDate ? data.map(Number) : data)
12,750✔
617
                .filter((v, i, self) => self.indexOf(v) === i);
129,459✔
618

619
        return isDate ? d.map(v => new Date(v)) : d;
12,750✔
620
}
621

622
/**
623
 * Merge array
624
 * @param {Array} arr Source array
625
 * @returns {Array}
626
 * @private
627
 */
628
function mergeArray(arr: any[]): any[] {
629
        return arr && arr.length ? arr.reduce((p, c) => p.concat(c)) : [];
10,986!
630
}
631

632
/**
633
 * Merge object returning new object
634
 * @param {object} target Target object
635
 * @param {object} objectN Source object
636
 * @returns {object} merged target object
637
 * @private
638
 */
639
function mergeObj(target: object, ...objectN): any {
640
        if (!objectN.length || (objectN.length === 1 && !objectN[0])) {
76,695✔
641
                return target;
44,943✔
642
        }
643

644
        const source = objectN.shift();
31,752✔
645

646
        if (isObject(target) && isObject(source)) {
31,752!
647
                Object.keys(source).forEach(key => {
31,752✔
648
                        if (!/^(__proto__|constructor|prototype)$/i.test(key)) {
121,989✔
649
                                const value = source[key];
121,986✔
650

651
                                if (isObject(value)) {
121,986✔
652
                                        !target[key] && (target[key] = {});
10,281✔
653
                                        target[key] = mergeObj(target[key], value);
10,281✔
654
                                } else {
655
                                        target[key] = isArray(value) ? value.concat() : value;
111,705✔
656
                                }
657
                        }
658
                });
659
        }
660

661
        return mergeObj(target, ...objectN);
31,752✔
662
}
663

664
/**
665
 * Sort value
666
 * @param {Array} data value to be sorted
667
 * @param {boolean} isAsc true: asc, false: desc
668
 * @returns {number|string|Date} sorted date
669
 * @private
670
 */
671
function sortValue(data: any[], isAsc = true): any[] {
12,771✔
672
        let fn;
673

674
        if (data[0] instanceof Date) {
26,373✔
675
                fn = isAsc ? (a, b) => a - b : (a, b) => b - a;
18,645✔
676
        } else {
677
                if (isAsc && !data.every(isNaN)) {
18,108✔
678
                        fn = (a, b) => a - b;
67,143✔
679
                } else if (!isAsc) {
120✔
680
                        fn = (a, b) => (a > b && -1) || (a < b && 1) || (a === b && 0);
12!
681
                }
682
        }
683

684
        return data.concat().sort(fn);
26,373✔
685
}
686

687
/**
688
 * Get min/max value
689
 * @param {string} type 'min' or 'max'
690
 * @param {Array} data Array data value
691
 * @returns {number|Date|undefined}
692
 * @private
693
 */
694
function getMinMax(type: "min" | "max", data: number[] | Date[] | any): number | Date | undefined
695
        | any {
696
        let res = data.filter(v => notEmpty(v));
599,781✔
697

698
        if (res.length) {
146,571✔
699
                if (isNumber(res[0])) {
146,337✔
700
                        res = Math[type](...res);
140,175✔
701
                } else if (res[0] instanceof Date) {
6,162!
702
                        res = sortValue(res, type === "min")[0];
6,162✔
703
                }
704
        } else {
705
                res = undefined;
234✔
706
        }
707

708
        return res;
146,571✔
709
}
710

711
/**
712
 * Get range
713
 * @param {number} start Start number
714
 * @param {number} end End number
715
 * @param {number} step Step number
716
 * @returns {Array}
717
 * @private
718
 */
719
const getRange = (start: number, end: number, step = 1): number[] => {
231✔
720
        const res: number[] = [];
1,197✔
721
        const n = Math.max(0, Math.ceil((end - start) / step)) | 0;
1,197✔
722

723
        for (let i = start; i < n; i++) {
1,197✔
724
                res.push(start + i * step);
20,433✔
725
        }
726

727
        return res;
1,197✔
728
};
729

730
// emulate event
731
const emulateEvent = {
231✔
732
        mouse: (() => {
733
                const getParams = () => ({
231✔
734
                        bubbles: false,
735
                        cancelable: false,
736
                        screenX: 0,
737
                        screenY: 0,
738
                        clientX: 0,
739
                        clientY: 0
740
                });
741

742
                try {
231✔
743
                        // eslint-disable-next-line no-new
744
                        new MouseEvent("t");
231✔
745

746
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
231!
747
                                el.dispatchEvent(new MouseEvent(eventType, params));
624✔
748
                        };
749
                } catch {
750
                        // Polyfills DOM4 MouseEvent
751
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
×
752
                                const mouseEvent = document.createEvent("MouseEvent");
×
753

754
                                // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
755
                                mouseEvent.initMouseEvent(
×
756
                                        eventType,
757
                                        params.bubbles,
758
                                        params.cancelable,
759
                                        window,
760
                                        0, // the event's mouse click count
761
                                        params.screenX,
762
                                        params.screenY,
763
                                        params.clientX,
764
                                        params.clientY,
765
                                        false,
766
                                        false,
767
                                        false,
768
                                        false,
769
                                        0,
770
                                        null
771
                                );
772

773
                                el.dispatchEvent(mouseEvent);
×
774
                        };
775
                }
776
        })(),
777
        touch: (el: SVGElement | HTMLElement, eventType: string, params: any) => {
778
                const touchObj = new Touch(mergeObj({
15✔
779
                        identifier: Date.now(),
780
                        target: el,
781
                        radiusX: 2.5,
782
                        radiusY: 2.5,
783
                        rotationAngle: 10,
784
                        force: 0.5
785
                }, params));
786

787
                el.dispatchEvent(new TouchEvent(eventType, {
15✔
788
                        cancelable: true,
789
                        bubbles: true,
790
                        shiftKey: true,
791
                        touches: [touchObj],
792
                        targetTouches: [],
793
                        changedTouches: [touchObj]
794
                }));
795
        }
796
};
797

798
/**
799
 * Process the template  & return bound string
800
 * @param {string} tpl Template string
801
 * @param {object} data Data value to be replaced
802
 * @returns {string}
803
 * @private
804
 */
805
function tplProcess(tpl: string, data: object): string {
806
        let res = tpl;
2,355✔
807

808
        for (const x in data) {
2,355✔
809
                res = res.replace(new RegExp(`{=${x}}`, "g"), data[x]);
5,841✔
810
        }
811

812
        return res;
2,355✔
813
}
814

815
/**
816
 * Get parsed date value
817
 * (It must be called in 'ChartInternal' context)
818
 * @param {Date|string|number} date Value of date to be parsed
819
 * @returns {Date}
820
 * @private
821
 */
822
function parseDate(date: Date | string | number | any): Date {
823
        let parsedDate;
824

825
        if (date instanceof Date) {
3,708✔
826
                parsedDate = date;
261✔
827
        } else if (isString(date)) {
3,447✔
828
                const {config, format} = this;
3,165✔
829

830
                // if fails to parse, try by new Date()
831
                // https://github.com/naver/billboard.js/issues/1714
832
                parsedDate = format.dataTime(config.data_xFormat)(date) ?? new Date(date);
3,165✔
833
        } else if (isNumber(date) && !isNaN(date)) {
282!
834
                parsedDate = new Date(+date);
282✔
835
        }
836

837
        if (!parsedDate || isNaN(+parsedDate)) {
3,708✔
838
                console && console.error &&
3✔
839
                        console.error(`Failed to parse x '${date}' to Date object`);
840
        }
841

842
        return parsedDate;
3,708✔
843
}
844

845
/**
846
 * Check if svg element has viewBox attribute
847
 * @param {d3Selection} svg Target svg selection
848
 * @returns {boolean}
849
 */
850
function hasViewBox(svg: d3Selection): boolean {
851
        const attr = svg.attr("viewBox");
1,536✔
852

853
        return attr ? /(\d+(\.\d+)?){3}/.test(attr) : false;
1,536!
854
}
855

856
/**
857
 * Determine if given node has the specified style
858
 * @param {d3Selection|SVGElement} node Target node
859
 * @param {object} condition Conditional style props object
860
 * @param {boolean} all If true, all condition should be matched
861
 * @returns {boolean}
862
 */
863
function hasStyle(node, condition: {[key: string]: string}, all = false): boolean {
2,847✔
864
        const isD3Node = !!node.node;
2,847✔
865
        let has = false;
2,847✔
866

867
        for (const [key, value] of Object.entries(condition)) {
2,847✔
868
                has = isD3Node ? node.style(key) === value : node.style[key] === value;
5,685!
869

870
                if (all === false && has) {
5,685✔
871
                        break;
9✔
872
                }
873
        }
874

875
        return has;
2,847✔
876
}
877

878
/**
879
 * Return if the current doc is visible or not
880
 * @returns {boolean}
881
 * @private
882
 */
883
function isTabVisible(): boolean {
884
        return document?.hidden === false || document?.visibilityState === "visible";
50,043!
885
}
886

887
/**
888
 * Get the current input type
889
 * @param {boolean} mouse Config value: interaction.inputType.mouse
890
 * @param {boolean} touch Config value: interaction.inputType.touch
891
 * @returns {string} "mouse" | "touch" | null
892
 * @private
893
 */
894
function convertInputType(mouse: boolean, touch: boolean): "mouse" | "touch" | null {
895
        const {DocumentTouch, matchMedia, navigator} = window;
96✔
896

897
        // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer#coarse
898
        const hasPointerCoarse = matchMedia?.("(pointer:coarse)").matches;
96✔
899
        let hasTouch = false;
96✔
900

901
        if (touch) {
96!
902
                // Some Edge desktop return true: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/20417074/
903
                if (navigator && "maxTouchPoints" in navigator) {
96!
904
                        hasTouch = navigator.maxTouchPoints > 0;
96✔
905

906
                        // Ref: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
907
                        // On IE11 with IE9 emulation mode, ('ontouchstart' in window) is returning true
908
                } else if (
×
909
                        "ontouchmove" in window || (DocumentTouch && document instanceof DocumentTouch)
×
910
                ) {
911
                        hasTouch = true;
×
912
                } else {
913
                        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#avoiding_user_agent_detection
914
                        if (hasPointerCoarse) {
×
915
                                hasTouch = true;
×
916
                        } else {
917
                                // Only as a last resort, fall back to user agent sniffing
918
                                const UA = navigator.userAgent;
×
919

920
                                hasTouch = /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
×
921
                                        /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
922
                        }
923
                }
924
        }
925

926
        // For non-touch device, media feature condition is: '(pointer:coarse) = false' and '(pointer:fine) = true'
927
        // https://github.com/naver/billboard.js/issues/3854#issuecomment-2404183158
928
        const hasMouse = mouse && !hasPointerCoarse && matchMedia?.("(pointer:fine)").matches;
96✔
929

930
        // fallback to 'mouse' if no input type is detected.
931
        return (hasMouse && "mouse") || (hasTouch && "touch") || "mouse";
96!
932
}
933

934
/**
935
 * Run function until given condition function return true
936
 * @param {Function} fn Function to be executed when condition is true
937
 * @param {Function} conditionFn Condition function to check if condition is true
938
 * @private
939
 */
940
function runUntil(fn: Function, conditionFn: Function): void {
941
        if (conditionFn() === false) {
4,448✔
942
                requestAnimationFrame(() => runUntil(fn, conditionFn));
4,112✔
943
        } else {
944
                fn();
336✔
945
        }
946
}
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