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

naver / billboard.js / 14591086771

22 Apr 2025 09:07AM UTC coverage: 87.071% (-7.1%) from 94.191%
14591086771

push

github

Jae Sung Park
fix(core): Fix potential security vulnerability

Add prevention for prototype pollution, by enforcing
options objects to not extend nor chaining Object.prototype

Fix #3975

5626 of 6953 branches covered (80.91%)

Branch coverage included in aggregate %.

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

389 existing lines in 27 files now uncovered.

7446 of 8060 relevant lines covered (92.38%)

11838.6 hits per line

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

83.26
/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
        getBoundingRect,
27
        getBrushSelection,
28
        getCssRules,
29
        getMinMax,
30
        getOption,
31
        getPathBox,
32
        getPointer,
33
        getRandom,
34
        getRange,
35
        getRectSegList,
36
        getScrollPosition,
37
        getTransformCTM,
38
        getTranslation,
39
        getUnique,
40
        hasStyle,
41
        hasValue,
42
        hasViewBox,
43
        isArray,
44
        isBoolean,
45
        isDefined,
46
        isEmpty,
47
        isFunction,
48
        isNumber,
49
        isObject,
50
        isObjectType,
51
        isString,
52
        isTabVisible,
53
        isUndefined,
54
        isValue,
55
        mergeArray,
56
        mergeObj,
57
        notEmpty,
58
        parseDate,
59
        runUntil,
60
        sanitize,
61
        setTextValue,
62
        sortValue,
63
        toArray,
64
        tplProcess
65
};
66

67
const isValue = (v: any): boolean => v || v === 0;
817,115✔
68
const isFunction = (v: unknown): v is (...args: any[]) => any => typeof v === "function";
682,470✔
69
const isString = (v: unknown): v is string => typeof v === "string";
2,555,610✔
70
const isNumber = (v: unknown): v is number => typeof v === "number";
1,473,147✔
71
const isUndefined = (v: unknown): v is undefined => typeof v === "undefined";
1,135,371✔
72
const isDefined = (v: unknown): boolean => typeof v !== "undefined";
1,545,757✔
73
const isBoolean = (v: unknown): boolean => typeof v === "boolean";
38,728✔
74
const ceil10 = (v: number): number => Math.ceil(v / 10) * 10;
23,625✔
75
const asHalfPixel = (n: number): number => Math.ceil(n) + 0.5;
24,630✔
76
const diffDomain = (d: number[]): number => d[1] - d[0];
11,980✔
77
const isObjectType = (v: unknown): v is Record<string | number, any> => typeof v === "object";
3,352,331✔
78
const isEmpty = (o: unknown): boolean => (
231✔
79
        isUndefined(o) || o === null ||
976,667✔
80
        (isString(o) && o.length === 0) ||
81
        (isObjectType(o) && !(o instanceof Date) && Object.keys(o).length === 0) ||
82
        (isNumber(o) && isNaN(o))
83
);
84
const notEmpty = (o: unknown): boolean => !isEmpty(o);
970,883✔
85

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

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

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

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

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

127
        return found;
426✔
128
}
129

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

305
        return selection;
846✔
306
}
307

308
/**
309
 * Get boundingClientRect.
310
 * Cache the evaluated value once it was called.
311
 * @param {HTMLElement} node Target element
312
 * @returns {object}
313
 * @private
314
 */
315
function getBoundingRect(
316
        node
317
): {
318
        left: number,
319
        top: number,
320
        right: number,
321
        bottom: number,
322
        x: number,
323
        y: number,
324
        width: number,
325
        height: number
326
} {
327
        const needEvaluate = !("rect" in node) || (
8,430!
328
                "rect" in node && node.hasAttribute("width") &&
329
                node.rect.width !== +node.getAttribute("width")
330
        );
331

332
        return needEvaluate ? (node.rect = node.getBoundingClientRect()) : node.rect;
8,430✔
333
}
334

335
/**
336
 * Retrun random number
337
 * @param {boolean} asStr Convert returned value as string
338
 * @param {number} min Minimum value
339
 * @param {number} max Maximum value
340
 * @returns {number|string}
341
 * @private
342
 */
343
function getRandom(asStr = true, min = 0, max = 10000) {
49,950✔
344
        const crpt = window.crypto || window.msCrypto;
16,715!
345
        const rand = crpt ?
16,715!
346
                min + crpt.getRandomValues(new Uint32Array(1))[0] % (max - min + 1) :
347
                Math.floor(Math.random() * (max - min) + min);
348

349
        return asStr ? String(rand) : rand;
16,715!
350
}
351

352
/**
353
 * Find index based on binary search
354
 * @param {Array} arr Data array
355
 * @param {number} v Target number to find
356
 * @param {number} start Start index of data array
357
 * @param {number} end End index of data arr
358
 * @param {boolean} isRotated Weather is roted axis
359
 * @returns {number} Index number
360
 * @private
361
 */
362
function findIndex(arr, v: number, start: number, end: number, isRotated: boolean): number {
363
        if (start > end) {
795✔
364
                return -1;
3✔
365
        }
366

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

370
        if (isRotated) {
792✔
371
                x = arr[mid].y;
108✔
372
                w = arr[mid].h;
108✔
373
        }
374

375
        if (v >= x && v <= x + w) {
792✔
376
                return mid;
411✔
377
        }
378

379
        return v < x ?
381✔
380
                findIndex(arr, v, start, mid - 1, isRotated) :
381
                findIndex(arr, v, mid + 1, end, isRotated);
382
}
383

384
/**
385
 * Check if brush is empty
386
 * @param {object} ctx Bursh context
387
 * @returns {boolean}
388
 * @private
389
 */
390
function brushEmpty(ctx): boolean {
391
        const selection = getBrushSelection(ctx);
666✔
392

393
        if (selection) {
666✔
394
                // brush selected area
395
                // two-dimensional: [[x0, y0], [x1, y1]]
396
                // one-dimensional: [x0, x1] or [y0, y1]
397
                return selection[0] === selection[1];
207✔
398
        }
399

400
        return true;
459✔
401
}
402

403
/**
404
 * Deep copy object
405
 * @param {object} objectN Source object
406
 * @returns {object} Cloned object
407
 * @private
408
 */
409
function deepClone(...objectN) {
410
        const clone = v => {
2,895✔
411
                if (isObject(v) && v.constructor) {
1,118,085✔
412
                        const r = new v.constructor();
108,591✔
413

414
                        for (const k in v) {
108,591✔
415
                                r[k] = clone(v[k]);
1,092,030✔
416
                        }
417

418
                        return r;
108,591✔
419
                }
420

421
                return v;
1,009,494✔
422
        };
423

424
        return objectN.map(v => clone(v))
26,055✔
425
                .reduce((a, c) => (
426
                        {...a, ...c}
23,160✔
427
                ));
428
}
429

430
/**
431
 * Extend target from source object
432
 * @param {object} target Target object
433
 * @param {object|Array} source Source object
434
 * @returns {object}
435
 * @private
436
 */
437
function extend(target = {}, source): object {
×
438
        if (isArray(source)) {
71,778✔
439
                source.forEach(v => extend(target, v));
61,425✔
440
        }
441

442
        // exclude name with only numbers
443
        for (const p in source) {
71,778✔
444
                if (/^\d+$/.test(p) || p in target) {
500,640✔
445
                        continue;
381,276✔
446
                }
447

448
                target[p] = source[p];
119,364✔
449
        }
450

451
        return target;
71,778✔
452
}
453

454
/**
455
 * Return first letter capitalized
456
 * @param {string} str Target string
457
 * @returns {string} capitalized string
458
 * @private
459
 */
460
const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);
80,388✔
461

462
/**
463
 * Camelize from kebob style string
464
 * @param {string} str Target string
465
 * @param {string} separator Separator string
466
 * @returns {string} camelized string
467
 * @private
468
 */
469
function camelize(str: string, separator = "-"): string {
123✔
470
        return str.split(separator)
123✔
471
                .map((v, i) => (
472
                        i ? v.charAt(0).toUpperCase() + v.slice(1).toLowerCase() : v.toLowerCase()
171✔
473
                ))
474
                .join("");
475
}
476

477
/**
478
 * Convert to array
479
 * @param {object} v Target to be converted
480
 * @returns {Array}
481
 * @private
482
 */
483
const toArray = (v: CSSStyleDeclaration | any): any => [].slice.call(v);
6,858✔
484

485
/**
486
 * Add CSS rules
487
 * @param {object} style Style object
488
 * @param {string} selector Selector string
489
 * @param {Array} prop Prps arrary
490
 * @returns {number} Newely added rule index
491
 * @private
492
 */
493
function addCssRules(style, selector: string, prop: string[]): number {
494
        const {rootSelector = "", sheet} = style;
132✔
495
        const getSelector = s =>
132✔
496
                s
132✔
497
                        .replace(/\s?(bb-)/g, ".$1")
498
                        .replace(/\.+/g, ".");
499

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

502
        return sheet[sheet.insertRule ? "insertRule" : "addRule"](
132!
503
                rule,
504
                sheet.cssRules.length
505
        );
506
}
507

508
/**
509
 * Get css rules for specified stylesheets
510
 * @param {Array} styleSheets The stylesheets to get the rules from
511
 * @returns {Array}
512
 * @private
513
 */
514
function getCssRules(styleSheets: any[]) {
515
        let rules = [];
30✔
516

517
        styleSheets.forEach(sheet => {
30✔
518
                try {
99✔
519
                        if (sheet.cssRules && sheet.cssRules.length) {
99!
520
                                rules = rules.concat(toArray(sheet.cssRules));
99✔
521
                        }
522
                } catch (e) {
523
                        window.console?.warn(`Error while reading rules from ${sheet.href}: ${e.toString()}`);
×
524
                }
525
        });
526

527
        return rules;
30✔
528
}
529

530
/**
531
 * Get current window and container scroll position
532
 * @param {HTMLElement} node Target element
533
 * @returns {object} window scroll position
534
 * @private
535
 */
536
function getScrollPosition(node: HTMLElement) {
537
        return {
3,734✔
538
                x: (window.pageXOffset ?? window.scrollX ?? 0) + (node.scrollLeft ?? 0),
7,468!
539
                y: (window.pageYOffset ?? window.scrollY ?? 0) + (node.scrollTop ?? 0)
7,468!
540
        };
541
}
542

543
/**
544
 * Get translation string from screen <--> svg point
545
 * @param {SVGGraphicsElement} node graphics element
546
 * @param {number} x target x point
547
 * @param {number} y target y point
548
 * @param {boolean} inverse inverse flag
549
 * @returns {object}
550
 */
551
function getTransformCTM(node: SVGGraphicsElement, x = 0, y = 0, inverse = true): DOMPoint {
×
UNCOV
552
        const point = new DOMPoint(x, y);
×
UNCOV
553
        const screen = <DOMMatrix>node.getScreenCTM();
×
UNCOV
554
        const res = point.matrixTransform(
×
555
                inverse ? screen?.inverse() : screen
×
556
        );
557

UNCOV
558
        if (inverse === false) {
×
UNCOV
559
                const rect = node.getBoundingClientRect();
×
560

UNCOV
561
                res.x -= rect.x;
×
UNCOV
562
                res.y -= rect.y;
×
563
        }
564

UNCOV
565
        return res;
×
566
}
567

568
/**
569
 * Gets the SVGMatrix of an SVGGElement
570
 * @param {SVGElement} node Node element
571
 * @returns {SVGMatrix} matrix
572
 * @private
573
 */
574
function getTranslation(node) {
575
        const transform = node ? node.transform : null;
30!
576
        const baseVal = transform && transform.baseVal;
30✔
577

578
        return baseVal && baseVal.numberOfItems ?
30!
579
                baseVal.getItem(0).matrix :
580
                {a: 0, b: 0, c: 0, d: 0, e: 0, f: 0};
581
}
582

583
/**
584
 * Get unique value from array
585
 * @param {Array} data Source data
586
 * @returns {Array} Unique array value
587
 * @private
588
 */
589
function getUnique(data: any[]): any[] {
590
        const isDate = data[0] instanceof Date;
12,994✔
591
        const d = (isDate ? data.map(Number) : data)
12,994✔
592
                .filter((v, i, self) => self.indexOf(v) === i);
131,358✔
593

594
        return isDate ? d.map(v => new Date(v)) : d;
12,994✔
595
}
596

597
/**
598
 * Merge array
599
 * @param {Array} arr Source array
600
 * @returns {Array}
601
 * @private
602
 */
603
function mergeArray(arr: any[]): any[] {
604
        return arr && arr.length ? arr.reduce((p, c) => p.concat(c)) : [];
11,611!
605
}
606

607
/**
608
 * Merge object returning new object
609
 * @param {object} target Target object
610
 * @param {object} objectN Source object
611
 * @returns {object} merged target object
612
 * @private
613
 */
614
function mergeObj(target: object, ...objectN): any {
615
        if (!objectN.length || (objectN.length === 1 && !objectN[0])) {
77,314✔
616
                return target;
45,431✔
617
        }
618

619
        const source = objectN.shift();
31,883✔
620

621
        if (isObject(target) && isObject(source)) {
31,883!
622
                Object.keys(source).forEach(key => {
31,883✔
623
                        if (!/^(__proto__|constructor|prototype)$/i.test(key)) {
123,372✔
624
                                const value = source[key];
123,369✔
625

626
                                if (isObject(value)) {
123,369✔
627
                                        !target[key] && (target[key] = {});
10,152✔
628
                                        target[key] = mergeObj(target[key], value);
10,152✔
629
                                } else {
630
                                        target[key] = isArray(value) ? value.concat() : value;
113,217✔
631
                                }
632
                        }
633
                });
634
        }
635

636
        return mergeObj(target, ...objectN);
31,883✔
637
}
638

639
/**
640
 * Sort value
641
 * @param {Array} data value to be sorted
642
 * @param {boolean} isAsc true: asc, false: desc
643
 * @returns {number|string|Date} sorted date
644
 * @private
645
 */
646
function sortValue(data: any[], isAsc = true): any[] {
15,857✔
647
        let fn;
648

649
        if (data[0] instanceof Date) {
26,587✔
650
                fn = isAsc ? (a, b) => a - b : (a, b) => b - a;
18,645✔
651
        } else {
652
                if (isAsc && !data.every(isNaN)) {
18,322✔
653
                        fn = (a, b) => a - b;
67,692✔
654
                } else if (!isAsc) {
120✔
655
                        fn = (a, b) => (a > b && -1) || (a < b && 1) || (a === b && 0);
12!
656
                }
657
        }
658

659
        return data.concat().sort(fn);
26,587✔
660
}
661

662
/**
663
 * Get min/max value
664
 * @param {string} type 'min' or 'max'
665
 * @param {Array} data Array data value
666
 * @returns {number|Date|undefined}
667
 * @private
668
 */
669
function getMinMax(type: "min" | "max", data: number[] | Date[] | any): number | Date | undefined
670
        | any {
671
        let res = data.filter(v => notEmpty(v));
611,879✔
672

673
        if (res.length) {
149,899✔
674
                if (isNumber(res[0])) {
149,665✔
675
                        res = Math[type](...res);
143,503✔
676
                } else if (res[0] instanceof Date) {
6,162!
677
                        res = sortValue(res, type === "min")[0];
6,162✔
678
                }
679
        } else {
680
                res = undefined;
234✔
681
        }
682

683
        return res;
149,899✔
684
}
685

686
/**
687
 * Get range
688
 * @param {number} start Start number
689
 * @param {number} end End number
690
 * @param {number} step Step number
691
 * @returns {Array}
692
 * @private
693
 */
694
const getRange = (start: number, end: number, step = 1): number[] => {
231✔
695
        const res: number[] = [];
1,197✔
696
        const n = Math.max(0, Math.ceil((end - start) / step)) | 0;
1,197✔
697

698
        for (let i = start; i < n; i++) {
1,197✔
699
                res.push(start + i * step);
20,433✔
700
        }
701

702
        return res;
1,197✔
703
};
704

705
// emulate event
706
const emulateEvent = {
231✔
707
        mouse: (() => {
708
                const getParams = () => ({
231✔
709
                        bubbles: false,
710
                        cancelable: false,
711
                        screenX: 0,
712
                        screenY: 0,
713
                        clientX: 0,
714
                        clientY: 0
715
                });
716

717
                try {
231✔
718
                        // eslint-disable-next-line no-new
719
                        new MouseEvent("t");
231✔
720

721
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
231!
722
                                el.dispatchEvent(new MouseEvent(eventType, params));
624✔
723
                        };
724
                } catch {
725
                        // Polyfills DOM4 MouseEvent
726
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
×
727
                                const mouseEvent = document.createEvent("MouseEvent");
×
728

729
                                // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
730
                                mouseEvent.initMouseEvent(
×
731
                                        eventType,
732
                                        params.bubbles,
733
                                        params.cancelable,
734
                                        window,
735
                                        0, // the event's mouse click count
736
                                        params.screenX,
737
                                        params.screenY,
738
                                        params.clientX,
739
                                        params.clientY,
740
                                        false,
741
                                        false,
742
                                        false,
743
                                        false,
744
                                        0,
745
                                        null
746
                                );
747

748
                                el.dispatchEvent(mouseEvent);
×
749
                        };
750
                }
751
        })(),
752
        touch: (el: SVGElement | HTMLElement, eventType: string, params: any) => {
753
                const touchObj = new Touch(mergeObj({
15✔
754
                        identifier: Date.now(),
755
                        target: el,
756
                        radiusX: 2.5,
757
                        radiusY: 2.5,
758
                        rotationAngle: 10,
759
                        force: 0.5
760
                }, params));
761

762
                el.dispatchEvent(new TouchEvent(eventType, {
15✔
763
                        cancelable: true,
764
                        bubbles: true,
765
                        shiftKey: true,
766
                        touches: [touchObj],
767
                        targetTouches: [],
768
                        changedTouches: [touchObj]
769
                }));
770
        }
771
};
772

773
/**
774
 * Process the template  & return bound string
775
 * @param {string} tpl Template string
776
 * @param {object} data Data value to be replaced
777
 * @returns {string}
778
 * @private
779
 */
780
function tplProcess(tpl: string, data: object): string {
781
        let res = tpl;
2,385✔
782

783
        for (const x in data) {
2,385✔
784
                res = res.replace(new RegExp(`{=${x}}`, "g"), data[x]);
5,931✔
785
        }
786

787
        return res;
2,385✔
788
}
789

790
/**
791
 * Get parsed date value
792
 * (It must be called in 'ChartInternal' context)
793
 * @param {Date|string|number} date Value of date to be parsed
794
 * @returns {Date}
795
 * @private
796
 */
797
function parseDate(date: Date | string | number | any): Date {
798
        let parsedDate;
799

800
        if (date instanceof Date) {
3,708✔
801
                parsedDate = date;
261✔
802
        } else if (isString(date)) {
3,447✔
803
                const {config, format} = this;
3,165✔
804

805
                // if fails to parse, try by new Date()
806
                // https://github.com/naver/billboard.js/issues/1714
807
                parsedDate = format.dataTime(config.data_xFormat)(date) ?? new Date(date);
3,165✔
808
        } else if (isNumber(date) && !isNaN(date)) {
282!
809
                parsedDate = new Date(+date);
282✔
810
        }
811

812
        if (!parsedDate || isNaN(+parsedDate)) {
3,708✔
813
                console && console.error &&
3✔
814
                        console.error(`Failed to parse x '${date}' to Date object`);
815
        }
816

817
        return parsedDate;
3,708✔
818
}
819

820
/**
821
 * Check if svg element has viewBox attribute
822
 * @param {d3Selection} svg Target svg selection
823
 * @returns {boolean}
824
 */
825
function hasViewBox(svg: d3Selection): boolean {
826
        const attr = svg.attr("viewBox");
1,557✔
827

828
        return attr ? /(\d+(\.\d+)?){3}/.test(attr) : false;
1,557!
829
}
830

831
/**
832
 * Determine if given node has the specified style
833
 * @param {d3Selection|SVGElement} node Target node
834
 * @param {object} condition Conditional style props object
835
 * @param {boolean} all If true, all condition should be matched
836
 * @returns {boolean}
837
 */
838
function hasStyle(node, condition: {[key: string]: string}, all = false): boolean {
2,838✔
839
        const isD3Node = !!node.node;
2,838✔
840
        let has = false;
2,838✔
841

842
        for (const [key, value] of Object.entries(condition)) {
2,838✔
843
                has = isD3Node ? node.style(key) === value : node.style[key] === value;
5,667!
844

845
                if (all === false && has) {
5,667✔
846
                        break;
9✔
847
                }
848
        }
849

850
        return has;
2,838✔
851
}
852

853
/**
854
 * Return if the current doc is visible or not
855
 * @returns {boolean}
856
 * @private
857
 */
858
function isTabVisible(): boolean {
859
        return document?.hidden === false || document?.visibilityState === "visible";
110,371!
860
}
861

862
/**
863
 * Get the current input type
864
 * @param {boolean} mouse Config value: interaction.inputType.mouse
865
 * @param {boolean} touch Config value: interaction.inputType.touch
866
 * @returns {string} "mouse" | "touch" | null
867
 * @private
868
 */
869
function convertInputType(mouse: boolean, touch: boolean): "mouse" | "touch" | null {
870
        const {DocumentTouch, matchMedia, navigator} = window;
96✔
871

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

876
        if (touch) {
96!
877
                // Some Edge desktop return true: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/20417074/
878
                if (navigator && "maxTouchPoints" in navigator) {
96!
879
                        hasTouch = navigator.maxTouchPoints > 0;
96✔
880

881
                        // Ref: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
882
                        // On IE11 with IE9 emulation mode, ('ontouchstart' in window) is returning true
883
                } else if (
×
884
                        "ontouchmove" in window || (DocumentTouch && document instanceof DocumentTouch)
×
885
                ) {
886
                        hasTouch = true;
×
887
                } else {
888
                        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#avoiding_user_agent_detection
889
                        if (hasPointerCoarse) {
×
890
                                hasTouch = true;
×
891
                        } else {
892
                                // Only as a last resort, fall back to user agent sniffing
893
                                const UA = navigator.userAgent;
×
894

895
                                hasTouch = /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
×
896
                                        /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
897
                        }
898
                }
899
        }
900

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

905
        // fallback to 'mouse' if no input type is detected.
906
        return (hasMouse && "mouse") || (hasTouch && "touch") || "mouse";
96!
907
}
908

909
/**
910
 * Run function until given condition function return true
911
 * @param {Function} fn Function to be executed when condition is true
912
 * @param {Function} conditionFn Condition function to check if condition is true
913
 * @private
914
 */
915
function runUntil(fn: Function, conditionFn: Function): void {
916
        if (conditionFn() === false) {
9,852✔
917
                requestAnimationFrame(() => runUntil(fn, conditionFn));
9,515✔
918
        } else {
919
                fn();
337✔
920
        }
921
}
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