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

naver / billboard.js / 23128934000

16 Mar 2026 05:11AM UTC coverage: 93.732% (-0.01%) from 93.745%
23128934000

push

github

netil
chore: update CI node matrix, fix dprint and vite config

- Update CI node-version matrix to [22.x, 23.x, 24.x] for vite 8/rolldown compatibility
- Add --allow-no-files flag to dprint in lint-staged config
- Replace deprecated resolve.alias customResolver with resolveId plugin

Ref https://github.com/naver/billboard.js/actions/runs/23126207982/job/67176892044#step:6:9

6765 of 7515 branches covered (90.02%)

Branch coverage included in aggregate %.

8472 of 8741 relevant lines covered (96.92%)

25040.63 hits per line

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

85.69
/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
import {sanitize} from "./sanitize";
11

12
// ====================================
13
// Internal Helper (Not Exported)
14
// ====================================
15

16
/**
17
 * Get boundingClientRect or BBox with caching.
18
 * Internal helper for getBoundingRect() and getBBox()
19
 * @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
20
 * @param {SVGElement} node Target element
21
 * @param {boolean} forceEval Force evaluation
22
 * @returns {object}
23
 * @private
24
 */
25
function _getRect(
26
        relativeViewport: boolean,
27
        node: SVGElement & Partial<{rect: DOMRect | SVGRect}>,
28
        forceEval = false
×
29
): DOMRect | SVGRect {
30
        const _ = n => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
313,169✔
31

32
        if (forceEval) {
313,169✔
33
                return _(node);
286,646✔
34
        } else {
35
                // will cache the value if the element is not a SVGElement or the width is not set
36
                const needEvaluate = !("rect" in node) || (
26,523✔
37
                        "rect" in node && node.hasAttribute("width") &&
38
                        node.rect!.width !== +(node.getAttribute("width") || 0)
663!
39
                );
40

41
                return needEvaluate ? (node.rect = _(node)) : node.rect!;
26,523✔
42
        }
43
}
44

45
/**
46
 * Internal helper to iterate over array items and invoke a callback for each valid item
47
 * @param {Array} items Array to iterate
48
 * @param {function} callback Callback function (item, index) => void
49
 * @private
50
 */
51
function _forEachValidItem<T>(items: T[], callback: (item: T, index: number) => void): void {
52
        for (let i = 0; i < items.length; i++) {
71,448✔
53
                const item = items[i];
136,989✔
54

55
                if (item) {
136,989!
56
                        callback(item, i);
136,989✔
57
                }
58
        }
59
}
60

61
// ====================================
62
// Exported
63
// ====================================
64
const isValue = (v: any): boolean => v || v === 0;
2,001,422✔
65
const isFunction = (v: unknown): v is (...args: any[]) => any => typeof v === "function";
1,503,313✔
66
const isString = (v: unknown): v is string => typeof v === "string";
5,964,625✔
67
const isNumber = (v: unknown): v is number => typeof v === "number";
3,283,854✔
68
const isUndefined = (v: unknown): v is undefined => typeof v === "undefined";
2,721,780✔
69
const isDefined = (v: unknown): boolean => typeof v !== "undefined";
3,525,741✔
70
const isBoolean = (v: unknown): boolean => typeof v === "boolean";
83,089✔
71
const ceil10 = (v: number): number => Math.ceil(v / 10) * 10;
52,005✔
72
const asHalfPixel = (n: number): number => Math.ceil(n) + 0.5;
50,962✔
73
const diffDomain = (d: number[]): number => d[1] - d[0];
27,211✔
74
const isObjectType = (v: unknown): v is Record<string | number, any> => typeof v === "object";
8,047,045✔
75
const isEmptyObject = (obj: object): boolean => {
237✔
76
        for (const x in obj) {
424,731✔
77
                return false;
35,529✔
78
        }
79
        return true;
389,202✔
80
};
81
const isEmpty = (o: unknown): boolean => (
237✔
82
        isUndefined(o) || o === null ||
2,385,698✔
83
        (isString(o) && o.length === 0) ||
84
        (isObjectType(o) && !(o instanceof Date) && isEmptyObject(o)) ||
85
        (isNumber(o) && isNaN(o))
86
);
87
const notEmpty = (o: unknown): boolean => !isEmpty(o);
2,365,859✔
88

89
/**
90
 * Check if is array
91
 * @param {Array} arr Data to be checked
92
 * @returns {boolean}
93
 * @private
94
 */
95
const isArray = (arr: any): arr is any[] => Array.isArray(arr);
5,022,609✔
96

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

105
/**
106
 * Get specified key value from object
107
 * If default value is given, will return if given key value not found
108
 * @param {object} options Source object
109
 * @param {string} key Key value
110
 * @param {string|number|boolean|object|Array|function|null|undefined} defaultValue Default value
111
 * @returns {string|number|boolean|object|Array|function|null|undefined} Option value or default value
112
 * @private
113
 */
114
function getOption(options: object, key: string, defaultValue): any {
115
        return isDefined(options[key]) ? options[key] : defaultValue;
114,236✔
116
}
117

118
/**
119
 * Check if value exist in the given object
120
 * @param {object} dict Target object to be checked
121
 * @param {string|number|boolean|object|Array|function|null|undefined} value Value to be checked
122
 * @returns {boolean}
123
 * @private
124
 */
125
function hasValue(dict: object, value: any): boolean {
126
        let found = false;
561✔
127

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

130
        return found;
561✔
131
}
132

133
/**
134
 * Call function with arguments
135
 * @param {function} fn Function to be called
136
 * @param {object|null|undefined} thisArg "this" value for fn
137
 * @param {...(string|number|boolean|object|Array|function|null|undefined)} args Arguments for fn
138
 * @returns {boolean} true: fn is function, false: fn is not function
139
 * @private
140
 */
141
function callFn(fn: unknown, thisArg: any, ...args: any[]): boolean {
142
        const isFn = isFunction(fn);
29,124✔
143

144
        isFn && fn.call(thisArg, ...args);
29,124✔
145
        return isFn;
29,124✔
146
}
147

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

157
        const end = function(...args) {
1,161✔
158
                !--n && cb.apply(this, ...args);
2,715✔
159
        };
160

161
        // if is transition selection
162
        if ("duration" in transition) {
1,161✔
163
                transition
1,077✔
164
                        .each(() => ++n)
2,796✔
165
                        .on("end", end);
166
        } else {
167
                ++n;
84✔
168
                transition.call(end);
84✔
169
        }
170
}
171

172
/**
173
 * Set text value. If there're multiline add nodes.
174
 * @param {d3Selection} node Text node
175
 * @param {string} text Text value string
176
 * @param {Array} dy dy value for multilined text
177
 * @param {boolean} toMiddle To be alingned vertically middle
178
 * @private
179
 */
180
function setTextValue(
181
        node: d3Selection,
182
        text: string,
183
        dy: number[] = [-1, 1],
1,257✔
184
        toMiddle: boolean = false
735✔
185
) {
186
        if (!node || !isString(text)) {
4,119!
187
                return;
×
188
        }
189

190
        if (text.indexOf("\n") === -1) {
4,119✔
191
                node.text(text);
3,309✔
192
        } else {
193
                const diff = [node.text(), text].map(v => v.replace(/[\s\n]/g, ""));
1,620✔
194

195
                if (diff[0] !== diff[1]) {
810✔
196
                        const multiline = text.split("\n");
792✔
197
                        const len = toMiddle ? multiline.length - 1 : 1;
792✔
198

199
                        // reset possible text
200
                        node.html("");
792✔
201

202
                        multiline.forEach((v, i) => {
792✔
203
                                node.append("tspan")
1,644✔
204
                                        .attr("x", 0)
205
                                        .attr("dy", `${i === 0 ? dy[0] * len : dy[1]}em`)
1,644✔
206
                                        .text(v);
207
                        });
208
                }
209
        }
210
}
211

212
/**
213
 * Substitution of SVGPathSeg API polyfill
214
 * @param {SVGGraphicsElement} path Target svg element
215
 * @returns {Array}
216
 * @private
217
 */
218
function getRectSegList(path: SVGGraphicsElement): {x: number, y: number}[] {
219
        /*
220
         * seg1 ---------- seg2
221
         *   |               |
222
         *   |               |
223
         *   |               |
224
         * seg0 ---------- seg3
225
         */
226
        const {x, y, width, height} = path.getBBox();
540✔
227

228
        return [
540✔
229
                {x, y: y + height}, // seg0
230
                {x, y}, // seg1
231
                {x: x + width, y}, // seg2
232
                {x: x + width, y: y + height} // seg3
233
        ];
234
}
235

236
/**
237
 * Get svg bounding path box dimension
238
 * @param {SVGGraphicsElement} path Target svg element
239
 * @returns {object}
240
 * @private
241
 */
242
function getPathBox(
243
        path: SVGGraphicsElement
244
): {x: number, y: number, width: number, height: number} {
245
        const {width, height} = getBoundingRect(path);
267✔
246
        const items = getRectSegList(path);
267✔
247
        const x = items[0].x;
267✔
248
        const y = Math.min(items[0].y, items[1].y);
267✔
249

250
        return {
267✔
251
                x,
252
                y,
253
                width,
254
                height
255
        };
256
}
257

258
/**
259
 * Get event's current position coordinates
260
 * @param {object} event Event object
261
 * @param {SVGElement|HTMLElement} element Target element
262
 * @returns {Array} [x, y] Coordinates x, y array
263
 * @private
264
 */
265
function getPointer(event, element?: HTMLElement | SVGElement): number[] {
266
        const touches = event &&
3,162✔
267
                (event.touches || (event.sourceEvent && event.sourceEvent.touches))?.[0];
5,583✔
268
        let pointer = [0, 0];
3,162✔
269

270
        try {
3,162✔
271
                pointer = d3Pointer(touches || event, element);
3,162✔
272
        } catch {}
273

274
        return pointer.map(v => (isNaN(v) ? 0 : v));
6,324✔
275
}
276

277
/**
278
 * Return brush selection array
279
 * @param {object} ctx Current instance
280
 * @returns {d3.brushSelection}
281
 * @private
282
 */
283
function getBrushSelection(ctx) {
284
        const {event, $el} = ctx;
912✔
285
        const main = $el.subchart.main || $el.main;
912!
286
        let selection;
287

288
        // check from event
289
        if (event && event.type === "brush") {
912!
290
                selection = event.selection;
×
291
                // check from brush area selection
292
        } else if (main && (selection = main.select(".bb-brush").node())) {
912!
293
                selection = d3BrushSelection(selection);
912✔
294
        }
295

296
        return selection;
912✔
297
}
298

299
/**
300
 * Get boundingClientRect.
301
 * @param {SVGElement} node Target element
302
 * @param {boolean} forceEval Force evaluation
303
 * @returns {object}
304
 * @private
305
 */
306
function getBoundingRect(node, forceEval = false) {
20,676✔
307
        return _getRect(true, node, forceEval);
269,093✔
308
}
309

310
/**
311
 * Get BBox.
312
 * @param {SVGElement} node Target element
313
 * @param {boolean} forceEval Force evaluation
314
 * @returns {object}
315
 * @private
316
 */
317
function getBBox(node, forceEval = false) {
2,883✔
318
        return _getRect(false, node, forceEval);
44,076✔
319
}
320

321
/**
322
 * Retrun random number
323
 * @param {boolean} asStr Convert returned value as string
324
 * @param {number} min Minimum value
325
 * @param {number} max Maximum value
326
 * @returns {number|string}
327
 * @private
328
 */
329
function getRandom(asStr = true, min = 0, max = 10000) {
88,032✔
330
        const crpt = window.crypto || window.msCrypto;
29,624!
331
        const rand = crpt ?
29,624!
332
                min + crpt.getRandomValues(new Uint32Array(1))[0] % (max - min + 1) :
333
                Math.floor(Math.random() * (max - min) + min);
334

335
        return asStr ? String(rand) : rand;
29,624!
336
}
337

338
/**
339
 * Find index based on binary search
340
 * @param {Array} arr Data array
341
 * @param {number} v Target number to find
342
 * @param {number} start Start index of data array
343
 * @param {number} end End index of data arr
344
 * @param {boolean} isRotated Weather is roted axis
345
 * @returns {number} Index number
346
 * @private
347
 */
348
function findIndex(arr, v: number, start: number, end: number, isRotated: boolean): number {
349
        if (start > end) {
3,021✔
350
                return -1;
27✔
351
        }
352

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

356
        if (isRotated) {
2,994✔
357
                x = arr[mid].y;
201✔
358
                w = arr[mid].h;
201✔
359
        }
360

361
        if (v >= x && v <= x + w) {
2,994✔
362
                return mid;
1,491✔
363
        }
364

365
        return v < x ?
1,503✔
366
                findIndex(arr, v, start, mid - 1, isRotated) :
367
                findIndex(arr, v, mid + 1, end, isRotated);
368
}
369

370
/**
371
 * Check if brush is empty
372
 * @param {object} ctx Bursh context
373
 * @returns {boolean}
374
 * @private
375
 */
376
function brushEmpty(ctx): boolean {
377
        const selection = getBrushSelection(ctx);
717✔
378

379
        if (selection) {
717✔
380
                // brush selected area
381
                // two-dimensional: [[x0, y0], [x1, y1]]
382
                // one-dimensional: [x0, x1] or [y0, y1]
383
                return selection[0] === selection[1];
222✔
384
        }
385

386
        return true;
495✔
387
}
388

389
/**
390
 * Deep copy object
391
 * @param {object} objectN Source object
392
 * @returns {object} Cloned object
393
 * @private
394
 */
395
function deepClone(...objectN) {
396
        const clone = v => {
6,294✔
397
                if (isObject(v) && v.constructor) {
2,572,329✔
398
                        const r = new v.constructor();
237,753✔
399

400
                        for (const k in v) {
237,753✔
401
                                r[k] = clone(v[k]);
2,515,683✔
402
                        }
403

404
                        return r;
237,753✔
405
                }
406

407
                return v;
2,334,576✔
408
        };
409

410
        return objectN.map(v => clone(v))
56,646✔
411
                .reduce((a, c) => (
412
                        {...a, ...c}
50,352✔
413
                ));
414
}
415

416
/**
417
 * Extend target from source object
418
 * @param {object} target Target object
419
 * @param {object|Array} source Source object
420
 * @returns {object}
421
 * @private
422
 */
423
function extend(target = {}, source): object {
×
424
        if (isArray(source)) {
72,780✔
425
                source.forEach(v => extend(target, v));
62,283✔
426
        }
427

428
        // exclude name with only numbers
429
        for (const p in source) {
72,780✔
430
                if (/^\d+$/.test(p) || p in target) {
512,634✔
431
                        continue;
390,528✔
432
                }
433

434
                target[p] = source[p];
122,106✔
435
        }
436

437
        return target;
72,780✔
438
}
439

440
/**
441
 * Return first letter capitalized
442
 * @param {string} str Target string
443
 * @returns {string} capitalized string
444
 * @private
445
 */
446
const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);
199,374✔
447

448
/**
449
 * Camelize from kebob style string
450
 * @param {string} str Target string
451
 * @param {string} separator Separator string
452
 * @returns {string} camelized string
453
 * @private
454
 */
455
function camelize(str: string, separator = "-"): string {
123✔
456
        return str.split(separator)
123✔
457
                .map((v, i) => (
458
                        i ? v.charAt(0).toUpperCase() + v.slice(1).toLowerCase() : v.toLowerCase()
171✔
459
                ))
460
                .join("");
461
}
462

463
/**
464
 * Convert to array
465
 * @param {object} v Target to be converted
466
 * @returns {Array}
467
 * @private
468
 */
469
const toArray = (v: CSSStyleDeclaration | any): any => [].slice.call(v);
9,297✔
470

471
/**
472
 * Add CSS rules
473
 * @param {object} style Style object
474
 * @param {string} selector Selector string
475
 * @param {Array} prop Prps arrary
476
 * @returns {number} Newely added rule index
477
 * @private
478
 */
479
function addCssRules(style, selector: string, prop: string[]): number {
480
        const {rootSelector = "", sheet} = style;
132✔
481
        const getSelector = s =>
132✔
482
                s
132✔
483
                        .replace(/\s?(bb-)/g, ".$1")
484
                        .replace(/\.+/g, ".");
485

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

488
        return sheet[sheet.insertRule ? "insertRule" : "addRule"](
132!
489
                rule,
490
                sheet.cssRules.length
491
        );
492
}
493

494
/**
495
 * Get css rules for specified stylesheets
496
 * @param {Array} styleSheets The stylesheets to get the rules from
497
 * @returns {Array}
498
 * @private
499
 */
500
function getCssRules(styleSheets: any[]) {
501
        let rules = [];
30✔
502

503
        styleSheets.forEach(sheet => {
30✔
504
                try {
99✔
505
                        if (sheet.cssRules && sheet.cssRules.length) {
99!
506
                                rules = rules.concat(toArray(sheet.cssRules));
99✔
507
                        }
508
                } catch (e) {
509
                        window.console?.warn(`Error while reading rules from ${sheet.href}: ${e.toString()}`);
×
510
                }
511
        });
512

513
        return rules;
30✔
514
}
515

516
/**
517
 * Get current window and container scroll position
518
 * @param {HTMLElement} node Target element
519
 * @returns {object} window scroll position
520
 * @private
521
 */
522
function getScrollPosition(node: HTMLElement) {
523
        return {
8,369✔
524
                x: (window.pageXOffset ?? window.scrollX ?? 0) + (node.scrollLeft ?? 0),
16,738!
525
                y: (window.pageYOffset ?? window.scrollY ?? 0) + (node.scrollTop ?? 0)
16,738!
526
        };
527
}
528

529
/**
530
 * Get translation string from screen <--> svg point
531
 * @param {SVGGraphicsElement} node graphics element
532
 * @param {number} x target x point
533
 * @param {number} y target y point
534
 * @param {boolean} inverse inverse flag
535
 * @returns {object}
536
 */
537
function getTransformCTM(node: SVGGraphicsElement, x = 0, y = 0, inverse = true): DOMPoint {
42!
538
        const point = new DOMPoint(x, y);
135✔
539
        const screen = <DOMMatrix>node.getScreenCTM();
135✔
540
        const res = point.matrixTransform(
135✔
541
                inverse ? screen?.inverse() : screen
135✔
542
        );
543

544
        if (inverse === false) {
135✔
545
                const rect = getBoundingRect(node);
93✔
546

547
                res.x -= rect.x;
93✔
548
                res.y -= rect.y;
93✔
549
        }
550

551
        return res;
135✔
552
}
553

554
/**
555
 * Gets the SVGMatrix of an SVGGElement
556
 * @param {SVGElement} node Node element
557
 * @returns {SVGMatrix} matrix
558
 * @private
559
 */
560
function getTranslation(node) {
561
        const transform = node ? node.transform : null;
90!
562
        const baseVal = transform && transform.baseVal;
90✔
563

564
        return baseVal && baseVal.numberOfItems ?
90✔
565
                baseVal.getItem(0).matrix :
566
                {a: 0, b: 0, c: 0, d: 0, e: 0, f: 0};
567
}
568

569
/**
570
 * Get position value from element's attribute or transform
571
 * @param {SVGElement} element SVG element
572
 * @param {string} type Coordinate type ("x" or "y")
573
 * @returns {number} Position value
574
 * @private
575
 */
576
function getElementPos(element: SVGElement | undefined, type: "x" | "y"): number {
577
        const attr = element?.getAttribute?.(type);
60✔
578

579
        if (attr) {
60!
580
                return parseFloat(attr);
×
581
        }
582

583
        const matrix = getTranslation(element);
60✔
584

585
        return type === "x" ? matrix.e : matrix.f;
60!
586
}
587

588
/**
589
 * Get unique value from array
590
 * @param {Array} data Source data
591
 * @returns {Array} Unique array value
592
 * @private
593
 */
594
function getUnique(data: any[]): any[] {
595
        const isDate = data[0] instanceof Date;
26,949✔
596
        const d = (isDate ? data.map(Number) : data)
26,949✔
597
                .filter((v, i, self) => self.indexOf(v) === i);
306,825✔
598

599
        return isDate ? d.map(v => new Date(v)) : d;
26,949✔
600
}
601

602
/**
603
 * Merge array
604
 * @param {Array} arr Source array
605
 * @returns {Array}
606
 * @private
607
 */
608
function mergeArray(arr: any[]): any[] {
609
        return arr && arr.length ? arr.reduce((p, c) => p.concat(c)) : [];
23,532!
610
}
611

612
/**
613
 * Merge object returning new object
614
 * @param {object} target Target object
615
 * @param {object} objectN Source object
616
 * @returns {object} merged target object
617
 * @private
618
 */
619
function mergeObj(target: object, ...objectN): any {
620
        if (!objectN.length || (objectN.length === 1 && !objectN[0])) {
178,795✔
621
                return target;
105,715✔
622
        }
623

624
        const source = objectN.shift();
73,080✔
625

626
        if (isObject(target) && isObject(source)) {
73,080!
627
                Object.keys(source).forEach(key => {
73,080✔
628
                        if (!/^(__proto__|constructor|prototype)$/i.test(key)) {
281,880✔
629
                                const value = source[key];
281,877✔
630

631
                                if (isObject(value)) {
281,877✔
632
                                        !target[key] && (target[key] = {});
24,171✔
633
                                        target[key] = mergeObj(target[key], value);
24,171✔
634
                                } else {
635
                                        target[key] = isArray(value) ? value.concat() : value;
257,706✔
636
                                }
637
                        }
638
                });
639
        }
640

641
        return mergeObj(target, ...objectN);
73,080✔
642
}
643

644
/**
645
 * Sort value
646
 * @param {Array} data value to be sorted
647
 * @param {boolean} isAsc true: asc, false: desc
648
 * @returns {number|string|Date} sorted date
649
 * @private
650
 */
651
function sortValue(data: any[], isAsc = true): any[] {
27,081✔
652
        let fn;
653

654
        if (data[0] instanceof Date) {
59,227✔
655
                fn = isAsc ? (a, b) => a - b : (a, b) => b - a;
134,748✔
656
        } else {
657
                if (isAsc && !data.every(isNaN)) {
39,259✔
658
                        fn = (a, b) => a - b;
199,853✔
659
                } else if (!isAsc) {
405✔
660
                        fn = (a, b) => (a > b && -1) || (a < b && 1) || (a === b && 0);
111!
661
                }
662
        }
663

664
        return data.concat().sort(fn);
59,227✔
665
}
666

667
/**
668
 * Get min/max value
669
 * @param {string} type 'min' or 'max'
670
 * @param {Array} data Array data value
671
 * @returns {number|Date|undefined}
672
 * @private
673
 */
674
function getMinMax(type: "min" | "max", data: number[] | Date[] | any): number | Date | undefined
675
        | any {
676
        let res = data.filter(v => notEmpty(v));
1,554,128✔
677

678
        if (res.length) {
345,142✔
679
                if (isNumber(res[0])) {
335,218✔
680
                        res = Math[type](...res);
319,744✔
681
                } else if (res[0] instanceof Date) {
15,474!
682
                        res = sortValue(res, type === "min")[0];
15,474✔
683
                }
684
        } else {
685
                res = undefined;
9,924✔
686
        }
687

688
        return res;
345,142✔
689
}
690

691
/**
692
 * Get range
693
 * @param {number} start Start number
694
 * @param {number} end End number
695
 * @param {number} step Step number
696
 * @returns {Array}
697
 * @private
698
 */
699
const getRange = (start: number, end: number, step = 1): number[] => {
237✔
700
        const res: number[] = [];
1,308✔
701
        const n = Math.max(0, Math.ceil((end - start) / step)) | 0;
1,308✔
702

703
        for (let i = start; i < n; i++) {
1,308✔
704
                res.push(start + i * step);
20,784✔
705
        }
706

707
        return res;
1,308✔
708
};
709

710
// emulate event
711
const emulateEvent = {
237✔
712
        mouse: (() => {
713
                const getParams = () => ({
237✔
714
                        bubbles: false,
715
                        cancelable: false,
716
                        screenX: 0,
717
                        screenY: 0,
718
                        clientX: 0,
719
                        clientY: 0
720
                });
721

722
                try {
237✔
723
                        // eslint-disable-next-line no-new
724
                        new MouseEvent("t");
237✔
725

726
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
237!
727
                                el.dispatchEvent(new MouseEvent(eventType, params));
939✔
728
                        };
729
                } catch {
730
                        // Polyfills DOM4 MouseEvent
731
                        return (el: SVGElement | HTMLElement, eventType: string, params = getParams()) => {
×
732
                                const mouseEvent = document.createEvent("MouseEvent");
×
733

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

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

767
                el.dispatchEvent(new TouchEvent(eventType, {
27✔
768
                        cancelable: true,
769
                        bubbles: true,
770
                        shiftKey: true,
771
                        touches: [touchObj],
772
                        targetTouches: [],
773
                        changedTouches: [touchObj]
774
                }));
775
        }
776
};
777

778
/**
779
 * Process the template  & return bound string
780
 * @param {string} tpl Template string
781
 * @param {object} data Data value to be replaced
782
 * @returns {string}
783
 * @private
784
 */
785
function tplProcess(tpl: string, data: object): string {
786
        let res = tpl;
3,726✔
787

788
        for (const x in data) {
3,726✔
789
                res = res.replace(new RegExp(`{=${x}}`, "g"), data[x]);
9,699✔
790
        }
791

792
        return sanitize(res);
3,726✔
793
}
794

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

805
        if (date instanceof Date) {
13,584✔
806
                parsedDate = date;
405✔
807
        } else if (isString(date)) {
13,179✔
808
                const {config, format} = this;
12,675✔
809

810
                // if fails to parse, try by new Date()
811
                // https://github.com/naver/billboard.js/issues/1714
812
                parsedDate = format.dataTime(config.data_xFormat)(date) ?? new Date(date);
12,675✔
813
        } else if (isNumber(date) && !isNaN(date)) {
504!
814
                parsedDate = new Date(+date);
504✔
815
        }
816

817
        if (!parsedDate || isNaN(+parsedDate)) {
13,584✔
818
                console && console.error &&
3✔
819
                        console.error(`Failed to parse x '${date}' to Date object`);
820
        }
821

822
        return parsedDate;
13,584✔
823
}
824

825
/**
826
 * Check if svg element has viewBox attribute
827
 * @param {d3Selection} svg Target svg selection
828
 * @returns {boolean}
829
 */
830
function hasViewBox(svg: d3Selection): boolean {
831
        const attr = svg.attr("viewBox");
4,071✔
832

833
        return attr ? /(\d+(\.\d+)?){3}/.test(attr) : false;
4,071✔
834
}
835

836
/**
837
 * Determine if given node has the specified style
838
 * @param {d3Selection|SVGElement} node Target node
839
 * @param {object} condition Conditional style props object
840
 * @param {boolean} all If true, all condition should be matched
841
 * @returns {boolean}
842
 */
843
function hasStyle(node, condition: Record<string, string>, all = false): boolean {
6,237✔
844
        const isD3Node = !!node.node;
6,237✔
845
        let has = false;
6,237✔
846

847
        for (const [key, value] of Object.entries(condition)) {
6,237✔
848
                has = isD3Node ? node.style(key) === value : node.style[key] === value;
12,465!
849

850
                if (all === false && has) {
12,465✔
851
                        break;
9✔
852
                }
853
        }
854

855
        return has;
6,237✔
856
}
857

858
/**
859
 * Return if the current doc is visible or not
860
 * @returns {boolean}
861
 * @private
862
 */
863
function isTabVisible(): boolean {
864
        return document?.hidden === false || document?.visibilityState === "visible";
126,209!
865
}
866

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

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

881
        if (touch) {
96!
882
                // Some Edge desktop return true: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/20417074/
883
                if (navigator && "maxTouchPoints" in navigator) {
96!
884
                        hasTouch = navigator.maxTouchPoints > 0;
96✔
885

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

900
                                hasTouch = /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
×
901
                                        /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
902
                        }
903
                }
904
        }
905

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

910
        // fallback to 'mouse' if no input type is detected.
911
        return (hasMouse && "mouse") || (hasTouch && "touch") || "mouse";
96!
912
}
913

914
/**
915
 * Run function until given condition function return true
916
 * @param {function} fn Function to be executed when condition is true
917
 * @param {function(): boolean} conditionFn Condition function to check if condition is true
918
 * @private
919
 */
920
function runUntil(fn: Function, conditionFn: Function): void {
921
        if (conditionFn() === false) {
11,798✔
922
                requestAnimationFrame(() => runUntil(fn, conditionFn));
11,360✔
923
        } else {
924
                fn();
438✔
925
        }
926
}
927

928
/**
929
 * Parse CSS shorthand values (padding, margin, border-radius, etc.)
930
 * @param {number|string|object} value Shorthand value(s)
931
 * @returns {object} Parsed object with top, right, bottom, left properties
932
 * @private
933
 */
934
function parseShorthand(
935
        value: number | string | object
936
): {top: number, right: number, bottom: number, left: number} {
937
        if (isObject(value) && !isString(value)) {
252✔
938
                const obj = value as {top?: number, right?: number, bottom?: number, left?: number};
36✔
939
                return {
36✔
940
                        top: obj.top || 0,
36!
941
                        right: obj.right || 0,
36!
942
                        bottom: obj.bottom || 0,
36!
943
                        left: obj.left || 0
36!
944
                };
945
        }
946

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

950
        return {top: a, right: b, bottom: c, left: d};
216✔
951
}
952

953
/**
954
 * Schedule a RAF update to batch multiple redraw requests
955
 * Manages a RAF state object to intelligently batch rapid updates while ensuring
956
 * immediate execution for the first call (for test compatibility)
957
 * @param {object} rafState RAF state object with pendingRaf property
958
 * @param {number|null} rafState.pendingRaf ID of pending RAF or null
959
 * @param {function} callback Function to execute in RAF
960
 * @returns {void}
961
 * @private
962
 */
963
function scheduleRAFUpdate(rafState: {pendingRaf: number | null}, callback: () => void): void {
964
        // If there's already a pending RAF, we're in a rapid update scenario
965
        // Cancel it and schedule a new one to batch the updates
966
        if (rafState.pendingRaf !== null) {
9!
967
                window.cancelAnimationFrame(rafState.pendingRaf);
×
968

969
                // Schedule new RAF
970
                rafState.pendingRaf = window.requestAnimationFrame(() => {
×
971
                        rafState.pendingRaf = null;
×
972
                        callback();
×
973
                });
974
        } else {
975
                // First call - execute immediately for test compatibility
976
                // But set pending RAF to detect rapid consecutive calls
977
                rafState.pendingRaf = window.requestAnimationFrame(() => {
9✔
978
                        rafState.pendingRaf = null;
9✔
979
                });
980

981
                callback();
9✔
982
        }
983
}
984

985
/**
986
 * Convert an array to a Set by applying a key extractor
987
 * @param {Array} items Array of items to convert to Set
988
 * @param {function} keyFn Function to extract key from each item (item, index) => key. Defaults to identity function
989
 * @returns {Set} Set with extracted keys
990
 * @private
991
 */
992
function toSet<T, K = T>(
993
        items: T[],
994
        keyFn: (item: T, index: number) => K = (item => item as unknown as K)
124,524✔
995
): Set<K> {
996
        const set = new Set<K>();
65,535✔
997

998
        _forEachValidItem(items, (item, i) => {
65,535✔
999
                set.add(keyFn(item, i));
124,524✔
1000
        });
1001

1002
        return set;
65,535✔
1003
}
1004

1005
/**
1006
 * Convert an array to a Map by applying key and value extractors
1007
 * @param {Array} items Array of items to convert to Map
1008
 * @param {function} keyFn Function to extract key from each item (item, index) => key
1009
 * @param {function} valueFn Function to extract value from each item (item, index) => value. Defaults to identity function
1010
 * @returns {Map} Map with extracted keys and values
1011
 * @private
1012
 */
1013
function toMap<T, K, V = T>(
1014
        items: T[],
1015
        keyFn: (item: T, index: number) => K,
1016
        valueFn: (item: T, index: number) => V = (item => item as unknown as V)
×
1017
): Map<K, V> {
1018
        const map = new Map<K, V>();
5,913✔
1019

1020
        _forEachValidItem(items, (item, i) => {
5,913✔
1021
                map.set(keyFn(item, i), valueFn(item, i));
12,465✔
1022
        });
1023

1024
        return map;
5,913✔
1025
}
1026

1027
export {
1028
        addCssRules,
1029
        asHalfPixel,
1030
        brushEmpty,
1031
        callFn,
1032
        camelize,
1033
        capitalize,
1034
        ceil10,
1035
        convertInputType,
1036
        deepClone,
1037
        diffDomain,
1038
        emulateEvent,
1039
        endall,
1040
        extend,
1041
        findIndex,
1042
        getBBox,
1043
        getBoundingRect,
1044
        getBrushSelection,
1045
        getCssRules,
1046
        getElementPos,
1047
        getMinMax,
1048
        getOption,
1049
        getPathBox,
1050
        getPointer,
1051
        getRandom,
1052
        getRange,
1053
        getRectSegList,
1054
        getScrollPosition,
1055
        getTransformCTM,
1056
        getTranslation,
1057
        getUnique,
1058
        hasStyle,
1059
        hasValue,
1060
        hasViewBox,
1061
        isArray,
1062
        isBoolean,
1063
        isDefined,
1064
        isEmpty,
1065
        isEmptyObject,
1066
        isFunction,
1067
        isNumber,
1068
        isObject,
1069
        isObjectType,
1070
        isString,
1071
        isTabVisible,
1072
        isUndefined,
1073
        isValue,
1074
        mergeArray,
1075
        mergeObj,
1076
        notEmpty,
1077
        parseDate,
1078
        parseShorthand,
1079
        runUntil,
1080
        sanitize,
1081
        scheduleRAFUpdate,
1082
        setTextValue,
1083
        sortValue,
1084
        toArray,
1085
        toMap,
1086
        toSet,
1087
        tplProcess
1088
};
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