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

naver / billboard.js / 21126911356

19 Jan 2026 05:55AM UTC coverage: 94.082% (-0.08%) from 94.157%
21126911356

push

github

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

update dependencies to the latest

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

6624 of 7321 branches covered (90.48%)

Branch coverage included in aggregate %.

8225 of 8462 relevant lines covered (97.2%)

25510.97 hits per line

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

86.19
/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
const isValue = (v: any): boolean => v || v === 0;
1,994,423✔
12
const isFunction = (v: unknown): v is (...args: any[]) => any => typeof v === "function";
1,495,283✔
13
const isString = (v: unknown): v is string => typeof v === "string";
5,956,740✔
14
const isNumber = (v: unknown): v is number => typeof v === "number";
3,270,572✔
15
const isUndefined = (v: unknown): v is undefined => typeof v === "undefined";
2,709,762✔
16
const isDefined = (v: unknown): boolean => typeof v !== "undefined";
3,483,189✔
17
const isBoolean = (v: unknown): boolean => typeof v === "boolean";
82,654✔
18
const ceil10 = (v: number): number => Math.ceil(v / 10) * 10;
50,778✔
19
const asHalfPixel = (n: number): number => Math.ceil(n) + 0.5;
50,854✔
20
const diffDomain = (d: number[]): number => d[1] - d[0];
27,097✔
21
const isObjectType = (v: unknown): v is Record<string | number, any> => typeof v === "object";
7,982,545✔
22
const isEmptyObject = (obj: object): boolean => {
234✔
23
        for (const x in obj) {
422,211✔
24
                return false;
35,262✔
25
        }
26
        return true;
386,949✔
27
};
28
const isEmpty = (o: unknown): boolean => (
234✔
29
        isUndefined(o) || o === null ||
2,376,863✔
30
        (isString(o) && o.length === 0) ||
31
        (isObjectType(o) && !(o instanceof Date) && isEmptyObject(o)) ||
32
        (isNumber(o) && isNaN(o))
33
);
34
const notEmpty = (o: unknown): boolean => !isEmpty(o);
2,357,183✔
35

36
/**
37
 * Check if is array
38
 * @param {Array} arr Data to be checked
39
 * @returns {boolean}
40
 * @private
41
 */
42
const isArray = (arr: any): arr is any[] => Array.isArray(arr);
5,008,991✔
43

44
/**
45
 * Check if is object
46
 * @param {object} obj Data to be checked
47
 * @returns {boolean}
48
 * @private
49
 */
50
const isObject = (obj: any): boolean => obj && !obj?.nodeType && isObjectType(obj) && !isArray(obj);
3,937,578✔
51

52
/**
53
 * Get specified key value from object
54
 * If default value is given, will return if given key value not found
55
 * @param {object} options Source object
56
 * @param {string} key Key value
57
 * @param {string|number|boolean|object|Array|function|null|undefined} defaultValue Default value
58
 * @returns {string|number|boolean|object|Array|function|null|undefined} Option value or default value
59
 * @private
60
 */
61
function getOption(options: object, key: string, defaultValue): any {
62
        return isDefined(options[key]) ? options[key] : defaultValue;
112,829✔
63
}
64

65
/**
66
 * Check if value exist in the given object
67
 * @param {object} dict Target object to be checked
68
 * @param {string|number|boolean|object|Array|function|null|undefined} value Value to be checked
69
 * @returns {boolean}
70
 * @private
71
 */
72
function hasValue(dict: object, value: any): boolean {
73
        let found = false;
561✔
74

75
        Object.keys(dict).forEach(key => (dict[key] === value) && (found = true));
1,629✔
76

77
        return found;
561✔
78
}
79

80
/**
81
 * Call function with arguments
82
 * @param {function} fn Function to be called
83
 * @param {object|null|undefined} thisArg "this" value for fn
84
 * @param {...(string|number|boolean|object|Array|function|null|undefined)} args Arguments for fn
85
 * @returns {boolean} true: fn is function, false: fn is not function
86
 * @private
87
 */
88
function callFn(fn: unknown, thisArg: any, ...args: any[]): boolean {
89
        const isFn = isFunction(fn);
28,897✔
90

91
        isFn && fn.call(thisArg, ...args);
28,897✔
92
        return isFn;
28,897✔
93
}
94

95
/**
96
 * Call function after all transitions ends
97
 * @param {d3.transition} transition Transition
98
 * @param {Fucntion} cb Callback function
99
 * @private
100
 */
101
function endall(transition, cb: Function): void {
102
        let n = 0;
1,161✔
103

104
        const end = function(...args) {
1,161✔
105
                !--n && cb.apply(this, ...args);
2,722✔
106
        };
107

108
        // if is transition selection
109
        if ("duration" in transition) {
1,161✔
110
                transition
1,077✔
111
                        .each(() => ++n)
2,796✔
112
                        .on("end", end);
113
        } else {
114
                ++n;
84✔
115
                transition.call(end);
84✔
116
        }
117
}
118

119
// Sanitize patterns (blacklist approach with repeated application)
120
const DANGEROUS_TAGS =
121
        "script|iframe|object|embed|form|input|button|textarea|select|style|link|meta|base|math|isindex";
234✔
122
const sanitizeRx = {
234✔
123
        tags: new RegExp(
124
                `<(${DANGEROUS_TAGS})\\b[\\s\\S]*?>([\\s\\S]*?<\\/(${DANGEROUS_TAGS})\\s*>)?`,
125
                "gi"
126
        ),
127
        // Handles: whitespace, slash, quotes before event handlers (e.g., <img/onerror=...>, <img src="x"onerror=...>)
128
        eventHandlers: /[\s/"']+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
129
        // Handles: javascript/data/vbscript URIs with optional whitespace/newlines between protocol and colon
130
        dangerousURIs: /(href|src|action|xlink:href)\s*=\s*["']?\s*(javascript|data|vbscript)\s*:/gi
131
};
132

133
/**
134
 * Sanitize HTML string to prevent XSS attacks
135
 * Uses blacklist approach with repeated application to prevent nested tag bypass
136
 * @param {string} str Target string value
137
 * @returns {string} Sanitized string with dangerous elements removed
138
 * @private
139
 */
140
function sanitize(str: string): string {
141
        if (!isString(str) || !str || str.indexOf("<") === -1) {
9,663✔
142
                return str;
5,100✔
143
        }
144

145
        let result = str;
4,563✔
146
        let prev: string;
147

148
        // Repeat until no more changes (prevents nested tag attacks like <scri<script>pt>)
149
        do {
4,563✔
150
                prev = result;
4,731✔
151
                result = result
4,731✔
152
                        .replace(sanitizeRx.tags, "")
153
                        .replace(sanitizeRx.eventHandlers, "")
154
                        .replace(sanitizeRx.dangerousURIs, "$1=\"\"");
155
        } while (result !== prev);
156

157
        return result;
4,563✔
158
}
159

160
/**
161
 * Set text value. If there're multiline add nodes.
162
 * @param {d3Selection} node Text node
163
 * @param {string} text Text value string
164
 * @param {Array} dy dy value for multilined text
165
 * @param {boolean} toMiddle To be alingned vertically middle
166
 * @private
167
 */
168
function setTextValue(
169
        node: d3Selection,
170
        text: string,
171
        dy: number[] = [-1, 1],
1,257✔
172
        toMiddle: boolean = false
735✔
173
) {
174
        if (!node || !isString(text)) {
4,119!
175
                return;
×
176
        }
177

178
        if (text.indexOf("\n") === -1) {
4,119✔
179
                node.text(text);
3,309✔
180
        } else {
181
                const diff = [node.text(), text].map(v => v.replace(/[\s\n]/g, ""));
1,620✔
182

183
                if (diff[0] !== diff[1]) {
810✔
184
                        const multiline = text.split("\n");
792✔
185
                        const len = toMiddle ? multiline.length - 1 : 1;
792✔
186

187
                        // reset possible text
188
                        node.html("");
792✔
189

190
                        multiline.forEach((v, i) => {
792✔
191
                                node.append("tspan")
1,644✔
192
                                        .attr("x", 0)
193
                                        .attr("dy", `${i === 0 ? dy[0] * len : dy[1]}em`)
1,644✔
194
                                        .text(v);
195
                        });
196
                }
197
        }
198
}
199

200
/**
201
 * Substitution of SVGPathSeg API polyfill
202
 * @param {SVGGraphicsElement} path Target svg element
203
 * @returns {Array}
204
 * @private
205
 */
206
function getRectSegList(path: SVGGraphicsElement): {x: number, y: number}[] {
207
        /*
208
         * seg1 ---------- seg2
209
         *   |               |
210
         *   |               |
211
         *   |               |
212
         * seg0 ---------- seg3
213
         */
214
        const {x, y, width, height} = path.getBBox();
540✔
215

216
        return [
540✔
217
                {x, y: y + height}, // seg0
218
                {x, y}, // seg1
219
                {x: x + width, y}, // seg2
220
                {x: x + width, y: y + height} // seg3
221
        ];
222
}
223

224
/**
225
 * Get svg bounding path box dimension
226
 * @param {SVGGraphicsElement} path Target svg element
227
 * @returns {object}
228
 * @private
229
 */
230
function getPathBox(
231
        path: SVGGraphicsElement
232
): {x: number, y: number, width: number, height: number} {
233
        const {width, height} = getBoundingRect(path);
267✔
234
        const items = getRectSegList(path);
267✔
235
        const x = items[0].x;
267✔
236
        const y = Math.min(items[0].y, items[1].y);
267✔
237

238
        return {
267✔
239
                x,
240
                y,
241
                width,
242
                height
243
        };
244
}
245

246
/**
247
 * Get event's current position coordinates
248
 * @param {object} event Event object
249
 * @param {SVGElement|HTMLElement} element Target element
250
 * @returns {Array} [x, y] Coordinates x, y array
251
 * @private
252
 */
253
function getPointer(event, element?: SVGElement): number[] {
254
        const touches = event &&
3,162✔
255
                (event.touches || (event.sourceEvent && event.sourceEvent.touches))?.[0];
5,583✔
256
        let pointer = [0, 0];
3,162✔
257

258
        try {
3,162✔
259
                pointer = d3Pointer(touches || event, element);
3,162✔
260
        } catch {}
261

262
        return pointer.map(v => (isNaN(v) ? 0 : v));
6,324✔
263
}
264

265
/**
266
 * Return brush selection array
267
 * @param {object} ctx Current instance
268
 * @returns {d3.brushSelection}
269
 * @private
270
 */
271
function getBrushSelection(ctx) {
272
        const {event, $el} = ctx;
912✔
273
        const main = $el.subchart.main || $el.main;
912!
274
        let selection;
275

276
        // check from event
277
        if (event && event.type === "brush") {
912!
278
                selection = event.selection;
×
279
                // check from brush area selection
280
        } else if (main && (selection = main.select(".bb-brush").node())) {
912!
281
                selection = d3BrushSelection(selection);
912✔
282
        }
283

284
        return selection;
912✔
285
}
286

287
// ====================================
288
// Internal Helper (Not Exported)
289
// ====================================
290

291
/**
292
 * Get boundingClientRect or BBox with caching.
293
 * Internal helper for getBoundingRect() and getBBox()
294
 * @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
295
 * @param {SVGElement} node Target element
296
 * @param {boolean} forceEval Force evaluation
297
 * @returns {object}
298
 * @private
299
 */
300
function _getRect(
301
        relativeViewport: boolean,
302
        node: SVGElement & Partial<{rect: DOMRect | SVGRect}>,
303
        forceEval = false
×
304
): DOMRect | SVGRect {
305
        const _ = n => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
311,728✔
306

307
        if (forceEval) {
311,728✔
308
                return _(node);
285,370✔
309
        } else {
310
                // will cache the value if the element is not a SVGElement or the width is not set
311
                const needEvaluate = !("rect" in node) || (
26,358✔
312
                        "rect" in node && node.hasAttribute("width") &&
313
                        node.rect!.width !== +(node.getAttribute("width") || 0)
663!
314
                );
315

316
                return needEvaluate ? (node.rect = _(node)) : node.rect!;
26,358✔
317
        }
318
}
319

320
/**
321
 * Internal helper to iterate over array items and invoke a callback for each valid item
322
 * @param {Array} items Array to iterate
323
 * @param {function} callback Callback function (item, index) => void
324
 * @private
325
 */
326
function _forEachValidItem<T>(items: T[], callback: (item: T, index: number) => void): void {
327
        for (let i = 0; i < items.length; i++) {
71,091✔
328
                const item = items[i];
136,470✔
329

330
                if (item) {
136,470!
331
                        callback(item, i);
136,470✔
332
                }
333
        }
334
}
335

336
// ====================================
337
// Exported
338
// ====================================
339

340
/**
341
 * Get boundingClientRect.
342
 * @param {SVGElement} node Target element
343
 * @param {boolean} forceEval Force evaluation
344
 * @returns {object}
345
 * @private
346
 */
347
function getBoundingRect(node, forceEval = false) {
20,529✔
348
        return _getRect(true, node, forceEval);
267,874✔
349
}
350

351
/**
352
 * Get BBox.
353
 * @param {SVGElement} node Target element
354
 * @param {boolean} forceEval Force evaluation
355
 * @returns {object}
356
 * @private
357
 */
358
function getBBox(node, forceEval = false) {
2,877✔
359
        return _getRect(false, node, forceEval);
43,854✔
360
}
361

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

376
        return asStr ? String(rand) : rand;
29,309!
377
}
378

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

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

397
        if (isRotated) {
2,994✔
398
                x = arr[mid].y;
201✔
399
                w = arr[mid].h;
201✔
400
        }
401

402
        if (v >= x && v <= x + w) {
2,994✔
403
                return mid;
1,491✔
404
        }
405

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

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

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

427
        return true;
495✔
428
}
429

430
/**
431
 * Deep copy object
432
 * @param {object} objectN Source object
433
 * @returns {object} Cloned object
434
 * @private
435
 */
436
function deepClone(...objectN) {
437
        const clone = v => {
6,231✔
438
                if (isObject(v) && v.constructor) {
2,534,064✔
439
                        const r = new v.constructor();
235,359✔
440

441
                        for (const k in v) {
235,359✔
442
                                r[k] = clone(v[k]);
2,477,985✔
443
                        }
444

445
                        return r;
235,359✔
446
                }
447

448
                return v;
2,298,705✔
449
        };
450

451
        return objectN.map(v => clone(v))
56,079✔
452
                .reduce((a, c) => (
453
                        {...a, ...c}
49,848✔
454
                ));
455
}
456

457
/**
458
 * Extend target from source object
459
 * @param {object} target Target object
460
 * @param {object|Array} source Source object
461
 * @returns {object}
462
 * @private
463
 */
464
function extend(target = {}, source): object {
×
465
        if (isArray(source)) {
72,780✔
466
                source.forEach(v => extend(target, v));
62,283✔
467
        }
468

469
        // exclude name with only numbers
470
        for (const p in source) {
72,780✔
471
                if (/^\d+$/.test(p) || p in target) {
508,521✔
472
                        continue;
386,643✔
473
                }
474

475
                target[p] = source[p];
121,878✔
476
        }
477

478
        return target;
72,780✔
479
}
480

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

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

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

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

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

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

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

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

554
        return rules;
30✔
555
}
556

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

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

585
        if (inverse === false) {
135✔
586
                const rect = getBoundingRect(node);
93✔
587

588
                res.x -= rect.x;
93✔
589
                res.y -= rect.y;
93✔
590
        }
591

592
        return res;
135✔
593
}
594

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

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

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

621
        return isDate ? d.map(v => new Date(v)) : d;
26,838✔
622
}
623

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

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

646
        const source = objectN.shift();
72,681✔
647

648
        if (isObject(target) && isObject(source)) {
72,681!
649
                Object.keys(source).forEach(key => {
72,681✔
650
                        if (!/^(__proto__|constructor|prototype)$/i.test(key)) {
280,683✔
651
                                const value = source[key];
280,680✔
652

653
                                if (isObject(value)) {
280,680✔
654
                                        !target[key] && (target[key] = {});
24,024✔
655
                                        target[key] = mergeObj(target[key], value);
24,024✔
656
                                } else {
657
                                        target[key] = isArray(value) ? value.concat() : value;
256,656✔
658
                                }
659
                        }
660
                });
661
        }
662

663
        return mergeObj(target, ...objectN);
72,681✔
664
}
665

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

676
        if (data[0] instanceof Date) {
59,038✔
677
                fn = isAsc ? (a, b) => a - b : (a, b) => b - a;
134,748✔
678
        } else {
679
                if (isAsc && !data.every(isNaN)) {
39,070✔
680
                        fn = (a, b) => a - b;
199,517✔
681
                } else if (!isAsc) {
405✔
682
                        fn = (a, b) => (a > b && -1) || (a < b && 1) || (a === b && 0);
111!
683
                }
684
        }
685

686
        return data.concat().sort(fn);
59,038✔
687
}
688

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

700
        if (res.length) {
343,978✔
701
                if (isNumber(res[0])) {
334,054✔
702
                        res = Math[type](...res);
318,580✔
703
                } else if (res[0] instanceof Date) {
15,474!
704
                        res = sortValue(res, type === "min")[0];
15,474✔
705
                }
706
        } else {
707
                res = undefined;
9,924✔
708
        }
709

710
        return res;
343,978✔
711
}
712

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

725
        for (let i = start; i < n; i++) {
1,308✔
726
                res.push(start + i * step);
20,784✔
727
        }
728

729
        return res;
1,308✔
730
};
731

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

744
                try {
234✔
745
                        // eslint-disable-next-line no-new
746
                        new MouseEvent("t");
234✔
747

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

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

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

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

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

810
        for (const x in data) {
3,726✔
811
                res = res.replace(new RegExp(`{=${x}}`, "g"), data[x]);
9,699✔
812
        }
813

814
        return sanitize(res);
3,726✔
815
}
816

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

827
        if (date instanceof Date) {
13,584✔
828
                parsedDate = date;
405✔
829
        } else if (isString(date)) {
13,179✔
830
                const {config, format} = this;
12,675✔
831

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

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

844
        return parsedDate;
13,584✔
845
}
846

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

855
        return attr ? /(\d+(\.\d+)?){3}/.test(attr) : false;
4,071✔
856
}
857

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

869
        for (const [key, value] of Object.entries(condition)) {
6,174✔
870
                has = isD3Node ? node.style(key) === value : node.style[key] === value;
12,339!
871

872
                if (all === false && has) {
12,339✔
873
                        break;
9✔
874
                }
875
        }
876

877
        return has;
6,174✔
878
}
879

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

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

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

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

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

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

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

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

936
/**
937
 * Run function until given condition function return true
938
 * @param {function} fn Function to be executed when condition is true
939
 * @param {function(): boolean} conditionFn Condition function to check if condition is true
940
 * @private
941
 */
942
function runUntil(fn: Function, conditionFn: Function): void {
943
        if (conditionFn() === false) {
11,573✔
944
                requestAnimationFrame(() => runUntil(fn, conditionFn));
11,130✔
945
        } else {
946
                fn();
443✔
947
        }
948
}
949

950
/**
951
 * Parse CSS shorthand values (padding, margin, border-radius, etc.)
952
 * @param {number|string|object} value Shorthand value(s)
953
 * @returns {object} Parsed object with top, right, bottom, left properties
954
 * @private
955
 */
956
function parseShorthand(
957
        value: number | string | object
958
): {top: number, right: number, bottom: number, left: number} {
959
        if (isObject(value) && !isString(value)) {
252✔
960
                const obj = value as {top?: number, right?: number, bottom?: number, left?: number};
36✔
961
                return {
36✔
962
                        top: obj.top || 0,
36!
963
                        right: obj.right || 0,
36!
964
                        bottom: obj.bottom || 0,
36!
965
                        left: obj.left || 0
36!
966
                };
967
        }
968

969
        const values = (isString(value) ? value.trim().split(/\s+/) : [value]).map(v => +v || 0);
270!
970
        const [a, b = a, c = a, d = b] = values;
216✔
971

972
        return {top: a, right: b, bottom: c, left: d};
216✔
973
}
974

975
/**
976
 * Schedule a RAF update to batch multiple redraw requests
977
 * Manages a RAF state object to intelligently batch rapid updates while ensuring
978
 * immediate execution for the first call (for test compatibility)
979
 * @param {object} rafState RAF state object with pendingRaf property
980
 * @param {number|null} rafState.pendingRaf ID of pending RAF or null
981
 * @param {function} callback Function to execute in RAF
982
 * @returns {void}
983
 * @private
984
 */
985
function scheduleRAFUpdate(rafState: {pendingRaf: number | null}, callback: () => void): void {
986
        // If there's already a pending RAF, we're in a rapid update scenario
987
        // Cancel it and schedule a new one to batch the updates
988
        if (rafState.pendingRaf !== null) {
9!
989
                window.cancelAnimationFrame(rafState.pendingRaf);
×
990

991
                // Schedule new RAF
992
                rafState.pendingRaf = window.requestAnimationFrame(() => {
×
993
                        rafState.pendingRaf = null;
×
994
                        callback();
×
995
                });
996
        } else {
997
                // First call - execute immediately for test compatibility
998
                // But set pending RAF to detect rapid consecutive calls
999
                rafState.pendingRaf = window.requestAnimationFrame(() => {
9✔
1000
                        rafState.pendingRaf = null;
9✔
1001
                });
1002

1003
                callback();
9✔
1004
        }
1005
}
1006

1007
/**
1008
 * Convert an array to a Set by applying a key extractor
1009
 * @param {Array} items Array of items to convert to Set
1010
 * @param {function} keyFn Function to extract key from each item (item, index) => key. Defaults to identity function
1011
 * @returns {Set} Set with extracted keys
1012
 * @private
1013
 */
1014
function toSet<T, K = T>(
1015
        items: T[],
1016
        keyFn: (item: T, index: number) => K = (item => item as unknown as K)
124,164✔
1017
): Set<K> {
1018
        const set = new Set<K>();
65,235✔
1019

1020
        _forEachValidItem(items, (item, i) => {
65,235✔
1021
                set.add(keyFn(item, i));
124,164✔
1022
        });
1023

1024
        return set;
65,235✔
1025
}
1026

1027
/**
1028
 * Convert an array to a Map by applying key and value extractors
1029
 * @param {Array} items Array of items to convert to Map
1030
 * @param {function} keyFn Function to extract key from each item (item, index) => key
1031
 * @param {function} valueFn Function to extract value from each item (item, index) => value. Defaults to identity function
1032
 * @returns {Map} Map with extracted keys and values
1033
 * @private
1034
 */
1035
function toMap<T, K, V = T>(
1036
        items: T[],
1037
        keyFn: (item: T, index: number) => K,
1038
        valueFn: (item: T, index: number) => V = (item => item as unknown as V)
×
1039
): Map<K, V> {
1040
        const map = new Map<K, V>();
5,856✔
1041

1042
        _forEachValidItem(items, (item, i) => {
5,856✔
1043
                map.set(keyFn(item, i), valueFn(item, i));
12,306✔
1044
        });
1045

1046
        return map;
5,856✔
1047
}
1048

1049
export {
1050
        addCssRules,
1051
        asHalfPixel,
1052
        brushEmpty,
1053
        callFn,
1054
        camelize,
1055
        capitalize,
1056
        ceil10,
1057
        convertInputType,
1058
        deepClone,
1059
        diffDomain,
1060
        emulateEvent,
1061
        endall,
1062
        extend,
1063
        findIndex,
1064
        getBBox,
1065
        getBoundingRect,
1066
        getBrushSelection,
1067
        getCssRules,
1068
        getMinMax,
1069
        getOption,
1070
        getPathBox,
1071
        getPointer,
1072
        getRandom,
1073
        getRange,
1074
        getRectSegList,
1075
        getScrollPosition,
1076
        getTransformCTM,
1077
        getTranslation,
1078
        getUnique,
1079
        hasStyle,
1080
        hasValue,
1081
        hasViewBox,
1082
        isArray,
1083
        isBoolean,
1084
        isDefined,
1085
        isEmpty,
1086
        isEmptyObject,
1087
        isFunction,
1088
        isNumber,
1089
        isObject,
1090
        isObjectType,
1091
        isString,
1092
        isTabVisible,
1093
        isUndefined,
1094
        isValue,
1095
        mergeArray,
1096
        mergeObj,
1097
        notEmpty,
1098
        parseDate,
1099
        parseShorthand,
1100
        runUntil,
1101
        sanitize,
1102
        scheduleRAFUpdate,
1103
        setTextValue,
1104
        sortValue,
1105
        toArray,
1106
        toMap,
1107
        toSet,
1108
        tplProcess
1109
};
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