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

naver / billboard.js / 15919719400

27 Jun 2025 06:24AM UTC coverage: 86.941% (-0.1%) from 87.071%
15919719400

push

github

netil
Merge branch 'master' into latest

5668 of 7016 branches covered (80.79%)

Branch coverage included in aggregate %.

158 of 188 new or added lines in 24 files covered. (84.04%)

1 existing line in 1 file now uncovered.

7487 of 8115 relevant lines covered (92.26%)

11762.65 hits per line

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

82.35
/src/ChartInternal/internals/tooltip.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 */
5
import {select as d3Select} from "d3-selection";
6
import {$ARC, $TOOLTIP} from "../../config/classes";
7
import {document} from "../../module/browser";
8
import {
9
        callFn,
10
        getBoundingRect,
11
        getPointer,
12
        getTransformCTM,
13
        hasViewBox,
14
        isEmpty,
15
        isFunction,
16
        isObject,
17
        isString,
18
        isUndefined,
19
        isValue,
20
        parseDate,
21
        sanitize,
22
        tplProcess
23
} from "../../module/util";
24
import type {IArcData, IDataRow} from "../data/IData";
25

26
export default {
27
        /**
28
         * Initializes the tooltip
29
         * @private
30
         */
31
        initTooltip(): void {
32
                const $$ = this;
2,814✔
33
                const {config, $el} = $$;
2,814✔
34

35
                $el.tooltip = d3Select(config.tooltip_contents.bindto);
2,814✔
36

37
                if ($el.tooltip.empty()) {
2,814✔
38
                        $el.tooltip = $el.chart
2,802✔
39
                                .append("div")
40
                                .attr("class", $TOOLTIP.tooltipContainer)
41
                                .style("position", "absolute")
42
                                .style("pointer-events", "none")
43
                                .style("display", "none");
44
                }
45

46
                $$.bindTooltipResizePos();
2,814✔
47
        },
48

49
        /**
50
         * Show tooltip at initialization.
51
         * Is called only when tooltip.init.show=true option is set
52
         * @private
53
         */
54
        initShowTooltip(): void {
55
                const $$ = this;
2,814✔
56
                const {config, $el, state: {hasAxis, hasRadar}} = $$;
2,814✔
57

58
                // Show tooltip if needed
59
                if (config.tooltip_init_show) {
2,814✔
60
                        const isArc = !(hasAxis || hasRadar);
18✔
61

62
                        if ($$.axis?.isTimeSeries() && isString(config.tooltip_init_x)) {
18✔
63
                                config.tooltip_init_x = parseDate.call($$, config.tooltip_init_x);
3✔
64
                        }
65

66
                        $$.api.tooltip.show({
18✔
67
                                data: {
68
                                        [isArc ? "index" : "x"]: config.tooltip_init_x
18✔
69
                                }
70
                        });
71

72
                        const position = config.tooltip_init_position;
18✔
73

74
                        if (!config.tooltip_contents.bindto && !isEmpty(position)) {
18✔
75
                                const {top = 0, left = 50} = position;
12!
76

77
                                $el.tooltip.style("top", isString(top) ? top : `${top}px`)
12!
78
                                        .style("left", isString(left) ? left : `${left}px`)
12!
79
                                        .style("display", null);
80
                        }
81
                }
82
        },
83

84
        /**
85
         * Get the tooltip HTML string
86
         * @param  {Array} args Arguments
87
         * @returns {string} Formatted HTML string
88
         * @private
89
         */
90
        getTooltipHTML(...args): string {
91
                const $$ = this;
453✔
92
                const {api, config} = $$;
453✔
93

94
                return isFunction(config.tooltip_contents) ?
453✔
95
                        config.tooltip_contents.bind(api)(...args) :
96
                        $$.getTooltipContent(...args);
97
        },
98

99
        /**
100
         * Returns the tooltip content(HTML string)
101
         * @param {object} d data
102
         * @param {Function} defaultTitleFormat Default title format
103
         * @param {Function} defaultValueFormat Default format for each data value in the tooltip.
104
         * @param {Function} color Color function
105
         * @returns {string} html
106
         * @private
107
         */
108
        getTooltipContent(d, defaultTitleFormat, defaultValueFormat, color): string {
109
                const $$ = this;
432✔
110
                const {api, config, state, $el} = $$;
432✔
111

112
                // get formatter function
113
                const [titleFn, nameFn, valueFn] = ["title", "name", "value"].map(v => {
432✔
114
                        const fn = config[`tooltip_format_${v}`];
1,296✔
115

116
                        return isFunction(fn) ? fn.bind(api) : fn;
1,296✔
117
                });
118

119
                // determine fotmatter function with sanitization
120
                const titleFormat = (...arg) => sanitize((titleFn || defaultTitleFormat)(...arg));
432✔
121
                const nameFormat = (...arg) => sanitize((nameFn || (name => name))(...arg));
822✔
122
                const valueFormat = (...arg) => {
432✔
123
                        const fn = valueFn || (
873✔
124
                                state.hasTreemap || $$.isStackNormalized() ?
2,253✔
125
                                        (v, ratio) => `${(ratio * 100).toFixed(2)}%` :
15✔
126
                                        defaultValueFormat
127
                        );
128

129
                        return sanitize(fn(...arg));
873✔
130
                };
131

132
                const order = config.tooltip_order;
432✔
133
                const getRowValue =
134
                        row => ($$.axis && $$.isBubbleZType(row) ?
1,893✔
135
                                $$.getBubbleZData(row.value, "z") :
136
                                $$.getBaseValue(row));
137
                const getBgColor = $$.levelColor ? row => $$.levelColor(row.value) : row => color(row);
822!
138
                const contents = config.tooltip_contents;
432✔
139
                const tplStr = contents.template;
432✔
140
                const targetIds = $$.mapToTargetIds();
432✔
141

142
                if (order === null && config.data_groups.length) {
432✔
143
                        // for stacked data, order should aligned with the visually displayed data
144
                        const ids = $$.orderTargets($$.data.targets)
48✔
145
                                .map(i2 => i2.id)
123✔
146
                                .reverse();
147

148
                        d.sort((a, b) => {
48✔
149
                                let v1 = a ? a.value : null;
90!
150
                                let v2 = b ? b.value : null;
90!
151

152
                                if (v1 > 0 && v2 > 0) {
90✔
153
                                        v1 = a.id ? ids.indexOf(a.id) : null;
66!
154
                                        v2 = b.id ? ids.indexOf(b.id) : null;
66!
155
                                }
156

157
                                return v1 - v2;
90✔
158
                        });
159
                } else if (/^(asc|desc)$/.test(order)) {
384✔
160
                        const isAscending = order === "asc";
36✔
161

162
                        d.sort((a, b) => {
36✔
163
                                const v1 = a ? getRowValue(a) : null;
126!
164
                                const v2 = b ? getRowValue(b) : null;
126!
165

166
                                return isAscending ? v1 - v2 : v2 - v1;
126✔
167
                        });
168
                } else if (isFunction(order)) {
348✔
169
                        d.sort(order.bind(api));
21✔
170
                }
171

172
                const tpl = $$.getTooltipContentTemplate(tplStr);
432✔
173
                const len = d.length;
432✔
174
                let text;
175
                let row;
176
                let param;
177
                let value;
178
                let i;
179

180
                for (i = 0; i < len; i++) {
432✔
181
                        row = d[i];
843✔
182

183
                        if (!row || !(getRowValue(row) || getRowValue(row) === 0)) {
843✔
184
                                continue;
21✔
185
                        }
186

187
                        if (isUndefined(text)) {
822✔
188
                                const title = (state.hasAxis || state.hasRadar) && titleFormat(row.x);
432✔
189

190
                                text = tplProcess(tpl[0], {
432✔
191
                                        CLASS_TOOLTIP: $TOOLTIP.tooltip,
192
                                        TITLE: isValue(title) ?
432✔
193
                                                (
194
                                                        tplStr ? title : `<tr><th colspan="2">${title}</th></tr>`
348✔
195
                                                ) :
196
                                                ""
197
                                });
198
                        }
199

200
                        if (!row.ratio && $el.arcs) {
822!
201
                                param = ["arc", $$.$el.arcs.select(`path.${$ARC.arc}-${row.id}`).data()[0]];
×
202
                                row.ratio = $$.getRatio(...param);
×
203
                        }
204

205
                        // arrange param to be passed to formatter
206
                        param = [row.ratio, row.id, row.index];
822✔
207

208
                        if ($$.isAreaRangeType(row)) {
822✔
209
                                const [high, low] = ["high", "low"].map(v =>
9✔
210
                                        valueFormat($$.getRangedData(row, v), ...param)
18✔
211
                                );
212
                                const mid = valueFormat(getRowValue(row), ...param);
9✔
213

214
                                value = `<b>Mid:</b> ${mid} <b>High:</b> ${high} <b>Low:</b> ${low}`;
9✔
215
                        } else if ($$.isCandlestickType(row)) {
813✔
216
                                const [open, high, low, close, volume] = ["open", "high", "low", "close", "volume"]
9✔
217
                                        .map(v => {
218
                                                const value = $$.getRangedData(row, v, "candlestick");
45✔
219

220
                                                return value ?
45✔
221
                                                        valueFormat(
222
                                                                $$.getRangedData(row, v, "candlestick"),
223
                                                                ...param
224
                                                        ) :
225
                                                        undefined;
226
                                        });
227

228
                                value =
9✔
229
                                        `<b>Open:</b> ${open} <b>High:</b> ${high} <b>Low:</b> ${low} <b>Close:</b> ${close}${
230
                                                volume ? ` <b>Volume:</b> ${volume}` : ""
9✔
231
                                        }`;
232
                        } else if ($$.isBarRangeType(row)) {
804✔
233
                                const {value: rangeValue, id, index} = row;
6✔
234

235
                                value = `${valueFormat(rangeValue, undefined, id, index)}`;
6✔
236
                        } else {
237
                                value = valueFormat(getRowValue(row), ...param);
798✔
238
                        }
239

240
                        if (value !== undefined) {
822!
241
                                // Skip elements when their name is set to null
242
                                if (row.name === null) {
822!
243
                                        continue;
×
244
                                }
245

246
                                const name = nameFormat(row.name ?? row.id, ...param);
822✔
247
                                const color = getBgColor(row);
822✔
248
                                const contentValue = {
822✔
249
                                        CLASS_TOOLTIP_NAME: $TOOLTIP.tooltipName + $$.getTargetSelectorSuffix(row.id),
250
                                        COLOR: (tplStr || !$$.patterns) ?
2,454!
251
                                                color :
252
                                                `<svg><rect style="fill:${color}" width="10" height="10"></rect></svg>`,
253
                                        NAME: name,
254
                                        VALUE: value
255
                                };
256

257
                                if (tplStr && isObject(contents.text)) {
822✔
258
                                        const index = targetIds.indexOf(row.id);
12✔
259

260
                                        Object.keys(contents.text).forEach(key => {
12✔
261
                                                contentValue[key] = contents.text[key][index];
12✔
262
                                        });
263
                                }
264

265
                                text += tplProcess(tpl[1], contentValue);
822✔
266
                        }
267
                }
268

269
                return `${text}</table>`;
432✔
270
        },
271

272
        /**
273
         * Get the content template string
274
         * @param {string} tplStr Tempalte string
275
         * @returns {Array} Template string
276
         * @private
277
         */
278
        getTooltipContentTemplate(tplStr?: string): string[] {
279
                return (tplStr || `<table class="{=CLASS_TOOLTIP}"><tbody>
432✔
280
                                {=TITLE}
281
                                {{<tr class="{=CLASS_TOOLTIP_NAME}">
282
                                        <td class="name">${
283
                        this.patterns ? `{=COLOR}` : `<span style="background-color:{=COLOR}"></span>`
423!
284
                }{=NAME}</td>
285
                                        <td class="value">{=VALUE}</td>
286
                                </tr>}}
287
                        </tbody></table>`)
288
                        .replace(/(\r?\n|\t)/g, "")
289
                        .split(/{{(.*)}}/);
290
        },
291

292
        /**
293
         * Update tooltip position coordinate
294
         * @param {object} dataToShow Data object
295
         * @param {SVGElement} eventTarget Event element
296
         * @private
297
         */
298
        setTooltipPosition(dataToShow: IDataRow[], eventTarget: SVGElement): void {
299
                const $$ = this;
525✔
300
                const {config, scale, state, $el: {eventRect, tooltip, svg}} = $$;
525✔
301
                const {bindto} = config.tooltip_contents;
525✔
302
                const isRotated = config.axis_rotated;
525✔
303
                const datum = tooltip?.datum();
525✔
304

305
                if (!bindto && datum) {
525✔
306
                        const data = dataToShow ?? JSON.parse(datum.current);
513!
307
                        const [x, y] = getPointer(state.event, eventTarget ?? eventRect?.node()); // get mouse event position
513!
308
                        const currPos: {
309
                                x: number,
310
                                y: number,
311
                                xAxis?: number,
312
                                yAxis?: number | (
313
                                        (value: number, id?: string, axisId?: string) => number
314
                                )
315
                        } = {x, y};
513✔
316

317
                        if (state.hasAxis && scale.x && datum && "x" in datum) {
513✔
318
                                const getYPos = (value = 0, id?: string, axisId = "y"): number => {
420!
319
                                        const scaleFn = scale[id ? $$.axis?.getId(id) : axisId];
213✔
320

321
                                        return scaleFn ?
213!
322
                                                scaleFn(value) + (isRotated ? state.margin.left : state.margin.top) :
213!
323
                                                0;
324
                                };
325

326
                                currPos.xAxis = scale.x(datum.x) + (
420✔
327
                                        // add margin only when user specified tooltip.position function
328
                                        config.tooltip_position ? (isRotated ? state.margin.top : state.margin.left) : 0
450!
329
                                );
330

331
                                if (data.length === 1) {
420✔
332
                                        currPos.yAxis = getYPos(data[0].value as number, data[0].id);
159✔
333
                                } else {
334
                                        currPos.yAxis = getYPos;
261✔
335
                                }
336
                        }
337

338
                        const {width = 0, height = 0} = datum;
513!
339

340
                        // Get tooltip position
341
                        const pos = config.tooltip_position?.bind($$.api)(
513✔
342
                                data,
343
                                width,
344
                                height,
345
                                eventRect?.node(),
346
                                currPos
347
                        ) ?? (
348
                                hasViewBox(svg) ?
483!
349
                                        $$.getTooltipPositionViewBox.bind($$)(width, height, currPos) :
350
                                        $$.getTooltipPosition.bind($$)(width, height, currPos)
351
                        );
352

353
                        ["top", "left"].forEach(v => {
513✔
354
                                const value = pos[v];
1,026✔
355

356
                                tooltip.style(v, `${value}px`);
1,026✔
357

358
                                // Remember left pos in percentage to be used on resize call
359
                                if (v === "left" && !datum.xPosInPercent) {
1,026✔
360
                                        datum.xPosInPercent = value / state.current.width * 100;
447✔
361
                                }
362
                        });
363
                }
364
        },
365

366
        /**
367
         * Get tooltip position when svg has vieBox attribute
368
         * @param {number} tWidth Tooltip width value
369
         * @param {number} tHeight Tooltip height value
370
         * @param {object} currPos Current event position value from SVG coordinate
371
         * @returns {object} top, left value
372
         */
373
        getTooltipPositionViewBox(tWidth: number, tHeight: number,
374
                currPos: {[key: string]: number}): {top: number, left: number} {
375
                const $$ = this;
×
376
                const {$el: {eventRect, svg}, config, state} = $$;
×
377

378
                const isRotated = config.axis_rotated;
×
379
                const hasArcType = $$.hasArcType() || state.hasFunnel || state.hasTreemap;
×
380
                const target = (hasArcType ? svg : eventRect)?.node() ?? state.event.target;
×
381

382
                let {x, y} = currPos;
×
383

384
                if (state.hasAxis) {
×
385
                        x = isRotated ? x : currPos.xAxis;
×
386
                        y = isRotated ? currPos.xAxis : y;
×
387
                }
388

389
                // currPos value based on SVG coordinate
390
                const ctm = getTransformCTM(target, x, y, false);
×
NEW
391
                const rect = getBoundingRect(target);
×
392
                const size = getTransformCTM(target, 20, 0, false).x;
×
393

394
                let top = ctm.y;
×
395
                let left = ctm.x + (tWidth / 2) + size;
×
396

397
                if (hasArcType) {
×
398
                        if (state.hasFunnel || state.hasTreemap || state.hasRadar) {
×
399
                                left -= (tWidth / 2) + size;
×
400
                                top += tHeight;
×
401
                        } else {
402
                                top += rect.height / 2;
×
403
                                left += (rect.width / 2) - (tWidth - size);
×
404
                        }
405
                }
406

407
                if (left + tWidth > rect.width) {
×
408
                        left = rect.width - tWidth - size;
×
409
                }
410

411
                if (top + tHeight > rect.height) {
×
412
                        top -= tHeight * 2;
×
413
                }
414

415
                return {
×
416
                        top,
417
                        left
418
                };
419
        },
420

421
        /**
422
         * Returns the position of the tooltip
423
         * @param {string} tWidth Width value of tooltip element
424
         * @param {string} tHeight Height value of tooltip element
425
         * @param {object} currPos Current mouse position
426
         * @returns {object} top, left value
427
         * @private
428
         */
429
        getTooltipPosition(tWidth: number, tHeight: number,
430
                currPos: {[key: string]: number}): {top: number, left: number} {
431
                const $$ = this;
483✔
432
                const {config, scale, state} = $$;
483✔
433
                const {width, height, current, hasFunnel, hasRadar, hasTreemap, isLegendRight, inputType} =
434
                        state;
483✔
435
                const hasGauge = $$.hasType("gauge") && !config.gauge_fullCircle;
483!
436
                const isRotated = config.axis_rotated;
483✔
437
                const hasArcType = $$.hasArcType();
483✔
438
                const svgLeft = $$.getSvgLeft(true);
483✔
439
                let chartRight = svgLeft + current.width - $$.getCurrentPaddingByDirection("right");
483✔
440

441
                const size = 20;
483✔
442
                let {x, y} = currPos;
483✔
443

444
                // Determine tooltip position
445
                if (hasRadar) {
483✔
446
                        x += x >= (width / 2) ? 15 : -(tWidth + 15);
12✔
447
                        y += 15;
12✔
448
                } else if (hasArcType) {
471✔
449
                        const notTouch = inputType !== "touch";
51✔
450

451
                        if (notTouch) {
51!
452
                                let titlePadding = $$.getTitlePadding?.() ?? 0;
51!
453

454
                                if (titlePadding && hasGauge && config.arc_rangeText_values?.length) {
51!
455
                                        titlePadding += 10;
×
456
                                }
457

458
                                x += (width - (isLegendRight ? $$.getLegendWidth() : 0)) / 2;
51!
459
                                y += (hasGauge ? height : (height / 2) + tHeight) + titlePadding;
51!
460
                        }
461
                } else if (hasFunnel || hasTreemap) {
420✔
462
                        y += tHeight;
30✔
463
                } else {
464
                        const padding = {
390✔
465
                                top: $$.getCurrentPaddingByDirection("top", true),
466
                                left: $$.getCurrentPaddingByDirection("left", true)
467
                        };
468

469
                        if (isRotated) {
390✔
470
                                x += svgLeft + padding.left + size;
48✔
471
                                y = padding.top + currPos.xAxis + size;
48✔
472
                                chartRight -= svgLeft;
48✔
473
                        } else {
474
                                x = svgLeft + padding.left + size + (scale.zoom ? x : currPos.xAxis);
342✔
475
                                y += padding.top - 5;
342✔
476
                        }
477
                }
478

479
                // when tooltip left + tWidth > chart's width
480
                if ((x + tWidth + 15) > chartRight) {
483✔
481
                        x -= tWidth + (hasFunnel || hasTreemap || hasArcType ? 0 : (isRotated ? size * 2 : 38));
75!
482
                }
483

484
                if (y + tHeight > current.height) {
483✔
485
                        const gap = hasTreemap ? tHeight + 10 : 30;
18!
486

487
                        y -= hasGauge ? tHeight * 1.5 : tHeight + gap;
18!
488
                }
489

490
                const pos = {top: y, left: x};
483✔
491

492
                // make sure to not be positioned out of viewport
493
                Object.keys(pos).forEach(v => {
483✔
494
                        if (pos[v] < 0) {
966✔
495
                                pos[v] = 0;
213✔
496
                        }
497
                });
498

499
                return pos;
483✔
500
        },
501

502
        /**
503
         * Show the tooltip
504
         * @param {object} selectedData Data object
505
         * @param {SVGElement} eventTarget Event element
506
         * @private
507
         */
508
        showTooltip(selectedData: IDataRow[], eventTarget: SVGElement): void {
509
                const $$ = this;
528✔
510
                const {config, $el: {tooltip}} = $$;
528✔
511
                const dataToShow = selectedData.filter(d => d && isValue($$.getBaseValue(d)));
966✔
512

513
                if (!tooltip || dataToShow.length === 0 || !config.tooltip_show) {
528✔
514
                        return;
6✔
515
                }
516

517
                let datum = tooltip.datum();
522✔
518
                const dataStr = JSON.stringify(selectedData);
522✔
519

520
                if (!datum || datum.current !== dataStr) {
522✔
521
                        const {index, x} = selectedData.concat().sort()[0];
453✔
522

523
                        callFn(config.tooltip_onshow, $$.api, selectedData);
453✔
524

525
                        // set tooltip content
526
                        tooltip
453✔
527
                                .html($$.getTooltipHTML(
528
                                        selectedData, // data
529
                                        $$.axis ? $$.axis.getXAxisTickFormat() : $$.categoryName.bind($$), // defaultTitleFormat
453✔
530
                                        $$.getDefaultValueFormat(), // defaultValueFormat
531
                                        $$.color // color
532
                                ))
533
                                .style("display", null)
534
                                .style("visibility", null) // for IE9
535
                                .datum(datum = {
536
                                        index,
537
                                        x,
538
                                        current: dataStr,
539
                                        width: tooltip.property("offsetWidth"),
540
                                        height: tooltip.property("offsetHeight")
541
                                });
542

543
                        callFn(config.tooltip_onshown, $$.api, selectedData);
453✔
544
                        $$._handleLinkedCharts(true, index);
453✔
545
                }
546

547
                $$.setTooltipPosition(dataToShow, eventTarget);
522✔
548
        },
549

550
        /**
551
         * Adjust tooltip position on resize event
552
         * @private
553
         */
554
        bindTooltipResizePos(): void {
555
                const $$ = this;
2,814✔
556
                const {resizeFunction, state, $el: {tooltip}} = $$;
2,814✔
557

558
                resizeFunction.add(() => {
2,814✔
559
                        if (tooltip.style("display") === "block") {
73✔
560
                                const {current} = state;
6✔
561
                                const {width, xPosInPercent} = tooltip.datum();
6✔
562
                                let value = current.width / 100 * xPosInPercent;
6✔
563
                                const diff = current.width - (value + width);
6✔
564

565
                                // if tooltip size overs current viewport size
566
                                if (diff < 0) {
6✔
567
                                        value += diff;
3✔
568
                                }
569

570
                                tooltip.style("left", `${value}px`);
6✔
571
                        }
572
                });
573
        },
574

575
        /**
576
         * Hide the tooltip
577
         * @param {boolean} force Force to hide
578
         * @private
579
         */
580
        hideTooltip(force?: boolean): void {
581
                const $$ = this;
345✔
582
                const {api, config, $el: {tooltip}} = $$;
345✔
583

584
                if (
345✔
585
                        tooltip && tooltip.style("display") !== "none" && (!config.tooltip_doNotHide || force)
768✔
586
                ) {
587
                        const selectedData = JSON.parse(tooltip.datum().current ?? {});
66!
588

589
                        callFn(config.tooltip_onhide, api, selectedData);
66✔
590

591
                        // hide tooltip
592
                        tooltip
66✔
593
                                .style("display", "none")
594
                                .style("visibility", "hidden") // for IE9
595
                                .datum(null);
596

597
                        callFn(config.tooltip_onhidden, api, selectedData);
66✔
598
                }
599
        },
600

601
        /**
602
         * Toggle display for linked chart instances
603
         * @param {boolean} show true: show, false: hide
604
         * @param {number} index x Axis index
605
         * @private
606
         */
607
        _handleLinkedCharts(show: boolean, index: number): void {
608
                const $$ = this;
516✔
609
                const {charts, config, state: {event}} = $$;
516✔
610

611
                // Prevent propagation among instances if isn't instantiated from the user's event
612
                // https://github.com/naver/billboard.js/issues/1979
613
                if (event?.isTrusted && config.tooltip_linked && charts.length > 1) {
516✔
614
                        const linkedName = config.tooltip_linked_name;
3✔
615

616
                        charts
3✔
617
                                .filter(c => c !== $$.api)
84✔
618
                                .forEach(c => {
619
                                        const {config, $el} = c.internal;
81✔
620
                                        const isLinked = config.tooltip_linked;
81✔
621
                                        const name = config.tooltip_linked_name;
81✔
622
                                        const isInDom = document.body.contains($el.chart.node());
81✔
623

624
                                        if (isLinked && linkedName === name && isInDom) {
81✔
625
                                                const data = $el.tooltip.data()[0];
6✔
626
                                                const isNotSameIndex = index !== data?.index;
6✔
627

628
                                                try {
6✔
629
                                                        c.tooltip[
6✔
630
                                                                show && isNotSameIndex ? "show" : "hide"
18!
631
                                                        ]({index});
632
                                                } catch {}
633
                                        }
634
                                });
635
                }
636
        },
637

638
        /**
639
         * Update tooltip content on redraw
640
         * - In a situation where tooltip is displayed and data load happens, it should reflect loaded data to tooltip
641
         * @param {d3Selection} context Event rect element
642
         * @param {number} index Data index
643
         * @private
644
         */
645
        updateTooltipOnRedraw(context?: SVGRectElement, index?: number): void {
646
                const $$ = this;
4,045✔
647
                const {
648
                        config,
649
                        $el: {eventRect, svg, tooltip},
650
                        state: {event, hasAxis, hasRadar, hasTreemap}
651
                } = $$;
4,045✔
652

653
                // Update tooltip, when tooltip is in shown state
654
                if (tooltip?.style("display") === "block" && event) {
4,045✔
655
                        const rect = context ?? (hasRadar ? svg : eventRect)?.node();
45✔
656

657
                        // for Axis based & Radar
658
                        if (hasAxis || hasRadar) {
45✔
659
                                if ($$.isMultipleX()) {
39✔
660
                                        $$.selectRectForMultipleXs(rect, false);
3✔
661
                                } else {
662
                                        const idx = index ?? $$.getDataIndexFromEvent(event);
36✔
663

664
                                        if (index === -1) {
36!
665
                                                $$.api.tooltip.hide();
×
666
                                        } else {
667
                                                $$.selectRectForSingle(rect, idx);
36✔
668
                                                $$.setExpand(idx, null, true);
36✔
669
                                        }
670
                                }
671
                        } else { // for Arc & Treemap
672
                                const {clientX, clientY} = event;
6✔
673

674
                                setTimeout(() => {
6✔
675
                                        let target = [clientX, clientY].every(Number.isFinite) &&
6✔
676
                                                document.elementFromPoint(clientX, clientY);
677
                                        const data = target && d3Select(target).datum() as IArcData;
6✔
678

679
                                        if (data) {
6!
680
                                                const d = $$.hasArcType() ?
6✔
681
                                                        $$.convertToArcData($$.updateAngle(data)) :
682
                                                        data?.data;
683

684
                                                hasTreemap && (target = svg.node());
6✔
685
                                                d && $$.showTooltip([d], target);
6✔
686
                                        } else {
687
                                                $$.api.tooltip.hide();
×
688
                                        }
689
                                }, config.transition_duration);
690
                        }
691
                }
692
        }
693
};
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