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

naver / billboard.js / 27122665529

08 Jun 2026 07:32AM UTC coverage: 92.54% (-1.1%) from 93.621%
27122665529

push

github

web-flow
feat(canvas): add canvas rendering mode

Add canvas entry, renderer engine, axis renderer, theme probing, and hit detection.
Support canvas flow, subchart, zoom, selection, grid/regions, export, tooltip, and focus.
Add tests, benchmarks, types, and docs for canvas mode limitations.

10455 of 11840 branches covered (88.3%)

Branch coverage included in aggregate %.

5094 of 5363 new or added lines in 68 files covered. (94.98%)

19 existing lines in 3 files now uncovered.

13349 of 13883 relevant lines covered (96.15%)

26556.19 hits per line

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

94.72
/src/ChartInternal/shape/shape.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 {
7
        curveBasis as d3CurveBasis,
8
        curveBasisClosed as d3CurveBasisClosed,
9
        curveBasisOpen as d3CurveBasisOpen,
10
        curveBundle as d3CurveBundle,
11
        curveCardinal as d3CurveCardinal,
12
        curveCardinalClosed as d3CurveCardinalClosed,
13
        curveCardinalOpen as d3CurveCardinalOpen,
14
        curveCatmullRom as d3CurveCatmullRom,
15
        curveCatmullRomClosed as d3CurveCatmullRomClosed,
16
        curveCatmullRomOpen as d3CurveCatmullRomOpen,
17
        curveLinear as d3CurveLinear,
18
        curveLinearClosed as d3CurveLinearClosed,
19
        curveMonotoneX as d3CurveMonotoneX,
20
        curveMonotoneY as d3CurveMonotoneY,
21
        curveNatural as d3CurveNatural,
22
        curveStep as d3CurveStep,
23
        curveStepAfter as d3CurveStepAfter,
24
        curveStepBefore as d3CurveStepBefore
25
} from "d3-shape";
26
import type {d3Selection} from "../../../types/types";
27
import CLASS from "../../config/classes";
28
import {KEY} from "../../module/Cache";
29
import {
30
        capitalize,
31
        getPointer,
32
        getRectSegList,
33
        getUnique,
34
        isFunction,
35
        isNumber,
36
        isObjectType,
37
        isUndefined,
38
        isValue,
39
        notEmpty,
40
        parseDate
41
} from "../../module/util";
42
import type {IDataIndice, IDataRow, TIndices} from "../data/IData";
43
import type {IOffset, ShapeElementConfig, UpdateTargetsConfig} from "./IShape";
44

45
// Module-level constant: avoids re-creating the lookup object on every getInterpolate() call
46
const CURVE_MAP: Record<string, unknown> = {
246✔
47
        basis: d3CurveBasis,
48
        "basis-closed": d3CurveBasisClosed,
49
        "basis-open": d3CurveBasisOpen,
50
        bundle: d3CurveBundle,
51
        cardinal: d3CurveCardinal,
52
        "cardinal-closed": d3CurveCardinalClosed,
53
        "cardinal-open": d3CurveCardinalOpen,
54
        "catmull-rom": d3CurveCatmullRom,
55
        "catmull-rom-closed": d3CurveCatmullRomClosed,
56
        "catmull-rom-open": d3CurveCatmullRomOpen,
57
        "monotone-x": d3CurveMonotoneX,
58
        "monotone-y": d3CurveMonotoneY,
59
        natural: d3CurveNatural,
60
        "linear-closed": d3CurveLinearClosed,
61
        linear: d3CurveLinear,
62
        step: d3CurveStep,
63
        "step-after": d3CurveStepAfter,
64
        "step-before": d3CurveStepBefore
65
};
66

67
// Re-export types for backward compatibility
68
export type {
69
        IOffset,
70
        LinearGradientOption,
71
        ShapeElementConfig,
72
        UpdateTargetsConfig
73
} from "./IShape";
74

75
/**
76
 * Check if a target can use line-like grouped point offsets.
77
 * @param {object} $$ ChartInternal instance
78
 * @param {object|string} d Data value, target or id
79
 * @returns {boolean} Whether target uses point-like y coordinates
80
 * @private
81
 */
82
function isLinePointGroupType($$, d): boolean {
83
        return $$.isLineType(d) || $$.isScatterType?.(d) || $$.isBubbleType?.(d);
12,474✔
84
}
85

86
/**
87
 * Get type filter for grouped line-like point offsets.
88
 * @param {object} $$ ChartInternal instance
89
 * @returns {function} Type filter
90
 * @private
91
 */
92
function getLinePointGroupTypeFilter($$): Function {
93
        return d => isLinePointGroupType($$, d);
10,674✔
94
}
95

96
/**
97
 * Get numeric value used for stacked offset calculation.
98
 * @param {object} $$ ChartInternal instance
99
 * @param {object} d Data row
100
 * @returns {number|Array|object|null} Offset value
101
 * @private
102
 */
103
function getShapeOffsetValue($$, d) {
104
        if ($$.isCandlestickType?.(d)) {
127,455✔
105
                return $$.getCandlestickData?.(d)?.close;
534✔
106
        }
107

108
        return $$.getBaseValue(d);
126,921✔
109
}
110

111
/**
112
 * Get grouped data point function for y coordinate
113
 * @param {object} d data vlaue
114
 * @returns {function|undefined}
115
 * @private
116
 */
117
function _getGroupedDataPointsFn(d) {
118
        const $$ = this;
1,776✔
119
        let fn;
120

121
        if (isLinePointGroupType($$, d)) {
1,776!
122
                const typeFilter = getLinePointGroupTypeFilter($$);
1,776✔
123

124
                fn = $$.generateGetLinePoints($$.getShapeIndices(typeFilter), false, typeFilter);
1,776✔
125
        } else if ($$.isBarType(d)) {
×
126
                fn = $$.generateGetBarPoints($$.getShapeIndices($$.isBarType));
×
NEW
127
        } else if ($$.isCandlestickType?.(d)) {
×
NEW
128
                fn = $$.generateGetCandlestickPoints?.($$.getShapeIndices($$.isCandlestickType));
×
129
        }
130

131
        return fn;
1,776✔
132
}
133

134
/**
135
 * Get shape color with gradient support
136
 * @param {object} d Data object
137
 * @param {string} configKey Configuration key for linearGradient (e.g., 'bar_linearGradient', 'area_linearGradient')
138
 * @param {(d: IDataRow) => string | null} colorFn Fallback color function when gradient is not enabled
139
 * @returns {string | null} Color string or gradient URL
140
 * @private
141
 */
142
export function getShapeColorWithGradient(
143
        this: any,
144
        d: IDataRow,
145
        configKey: string,
146
        colorFn: (d: IDataRow) => string | null
147
): string | null {
148
        return this.config[configKey] ? this.getGradienColortUrl(d.id) : colorFn(d);
30,321✔
149
}
150

151
/**
152
 * Initialize a shape element container
153
 * @param {ShapeElementConfig} config Configuration object
154
 * @private
155
 */
156
export function initShapeElement(this: any, config: ShapeElementConfig): void {
157
        const {$el} = this;
3,657✔
158
        const {elKey, className, cssRules, position} = config;
3,657✔
159
        const container = $el.main.select(`.${CLASS.chart}`);
3,657✔
160

161
        $el[elKey] = position === "first" ?
3,657!
162
                container.insert("g", ":first-child") :
163
                container.append("g");
164

165
        $el[elKey].attr("class", className);
3,657✔
166

167
        if (cssRules?.length) {
3,657✔
168
                $el[elKey].call(this.setCssRule(false, `.${className}`, cssRules));
3,606✔
169
        }
170
}
171

172
/**
173
 * Common update targets pattern for shapes
174
 * @param {Array} targets Target data
175
 * @param {UpdateTargetsConfig} config Configuration object
176
 * @returns {d3Selection} Enter selection for additional setup
177
 * @private
178
 */
179
export function updateTargetsForShape(
180
        this: any,
181
        targets: any[],
182
        config: UpdateTargetsConfig
183
): d3Selection {
184
        const $$ = this;
5,391✔
185
        const {$el} = $$;
5,391✔
186
        const {type, elKey, containerClass, itemClass, initFn, withFocus = true, withStyles = true} =
10,680✔
187
                config;
5,391✔
188

189
        if (!$el[elKey]) {
5,391✔
190
                initFn.call($$);
12✔
191
        }
192

193
        const classChart = $$.getChartClass(type);
5,391✔
194
        const classFocus = withFocus ? $$.classFocus.bind($$) : () => "";
5,391✔
195

196
        const mainUpdate = $el.main.select(`.${containerClass}`)
5,391✔
197
                .selectAll(`.${itemClass}`)
198
                .data($$.filterNullish(targets))
199
                .attr("class", d => classChart(d) + classFocus(d));
300✔
200

201
        const mainEnter = mainUpdate.enter().append("g")
5,391✔
202
                .attr("class", classChart);
203

204
        if (withStyles) {
5,391✔
205
                mainEnter
5,340✔
206
                        .style("opacity", "0")
207
                        .style("pointer-events", $$.getStylePropValue("none"));
208
        }
209

210
        return mainEnter;
5,391✔
211
}
212

213
export default {
214
        /**
215
         * Get the shape draw function
216
         * @returns {object}
217
         * @private
218
         */
219
        getDrawShape() {
220
                type TShape = {area?: any, bar?: any, line?: any};
221

222
                const $$ = this;
8,508✔
223
                const isRotated = $$.config.axis_rotated;
8,508✔
224
                const {hasRadar, hasTreemap} = $$.state;
8,508✔
225
                const shape = {type: <TShape>{}, indices: <TShape>{}, pos: {}};
8,508✔
226

227
                !hasTreemap && ["bar", "candlestick", "line", "area"].forEach(v => {
8,508✔
228
                        const name = capitalize(/^(bubble|scatter)$/.test(v) ? "line" : v);
33,492!
229

230
                        if (
33,492✔
231
                                $$.hasType(v) || $$.hasTypeOf(name) || (
91,716✔
232
                                        v === "line" &&
233
                                        ($$.hasType("bubble") || $$.hasType("scatter"))
234
                                )
235
                        ) {
236
                                const indices = $$.getShapeIndices($$[`is${name}Type`]);
8,088✔
237
                                const drawFn = $$[`generateDraw${name}`];
8,088✔
238

239
                                shape.indices[v] = indices;
8,088✔
240
                                shape.type[v] = drawFn ? drawFn.bind($$)(indices, false) : undefined;
8,088✔
241
                        }
242
                });
243

244
                if (!$$.hasArcType() || hasRadar || hasTreemap) {
8,508✔
245
                        let cx;
246
                        let cy;
247
                        let xForText;
248
                        let yForText;
249

250
                        // generate circle x/y functions depending on updated params
251
                        if (!hasTreemap) {
7,728✔
252
                                cx = hasRadar ? $$.radarCircleX : (isRotated ? $$.circleY : $$.circleX);
7,593✔
253
                                cy = hasRadar ? $$.radarCircleY : (isRotated ? $$.circleX : $$.circleY);
7,593✔
254
                        }
255

256
                        if (hasTreemap && $$.state.isCanvasMode) {
7,728✔
257
                                xForText = yForText = function() {};
21✔
258
                        } else {
259
                                xForText = $$.generateXYForText(shape.indices, true);
7,707✔
260
                                yForText = $$.generateXYForText(shape.indices, false);
7,707✔
261
                        }
262

263
                        shape.pos = {
7,728✔
264
                                xForText,
265
                                yForText,
266
                                cx: (cx || function() {}).bind($$),
7,863✔
267
                                cy: (cy || function() {}).bind($$)
7,863✔
268
                        };
269
                }
270

271
                return shape;
8,508✔
272
        },
273

274
        /**
275
         * Get shape's indices according it's position within each axis tick.
276
         *
277
         * From the below example, indices will be:
278
         * ==> {data1: 0, data2: 0, data3: 1, data4: 1, __max__: 1}
279
         *
280
         *         data1 data3   data1 data3
281
         *         data2 data4   data2 data4
282
         *         -------------------------
283
         *                  0             1
284
         * @param {function} typeFilter Chart type filter function
285
         * @returns {object} Indices object with its position
286
         */
287
        getShapeIndices(typeFilter): TIndices {
288
                const $$ = this;
13,092✔
289
                const {config} = $$;
13,092✔
290
                const xs = config.data_xs;
13,092✔
291
                const hasXs = notEmpty(xs);
13,092✔
292
                const indices: TIndices = {};
13,092✔
293
                let i: any = hasXs ? {} : 0;
13,092✔
294

295
                if (hasXs) {
13,092✔
296
                        getUnique(Object.keys(xs).map(v => xs[v]))
252✔
297
                                .forEach(v => {
298
                                        i[v] = 0;
237✔
299
                                        indices[v] = {};
237✔
300
                                });
301
                }
302

303
                $$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$))
13,092✔
304
                        .forEach(d => {
305
                                const xKey = d.id in xs ? xs[d.id] : "";
20,076✔
306
                                const ind = xKey ? indices[xKey] : indices;
20,076✔
307

308
                                for (let j = 0, groups; (groups = config.data_groups[j]); j++) {
20,076✔
309
                                        if (groups.indexOf(d.id) < 0) {
10,863✔
310
                                                continue;
2,613✔
311
                                        }
312

313
                                        for (let k = 0, key; (key = groups[k]); k++) {
8,250✔
314
                                                if (key in ind) {
13,449✔
315
                                                        ind[d.id] = ind[key];
5,166✔
316
                                                        break;
5,166✔
317
                                                }
318

319
                                                // for same grouped data, add other data to same indices
320
                                                if (d.id !== key && xKey) {
8,283✔
321
                                                        ind[key] = ind[d.id] ?? i[xKey];
18✔
322
                                                }
323
                                        }
324
                                }
325

326
                                if (isUndefined(ind[d.id])) {
20,076✔
327
                                        ind[d.id] = xKey ? i[xKey]++ : i++;
14,910✔
328
                                        ind.__max__ = (xKey ? i[xKey] : i) - 1;
14,910✔
329
                                }
330
                        });
331

332
                return indices;
13,092✔
333
        },
334

335
        /**
336
         * Get indices value based on data ID value
337
         * @param {object} indices Indices object
338
         * @param {object} d Data row
339
         * @param {string} caller Caller function name (Used only for 'sparkline' plugin)
340
         * @returns {object} Indices object
341
         * @private
342
         */
343
        getIndices(indices: TIndices, d: IDataRow, caller?: string): IDataIndice { // eslint-disable-line
344
                const $$ = this;
116,754✔
345
                const {data_xs: xs, bar_indices_removeNull: removeNull} = $$.config;
116,754✔
346
                const {id, index} = d;
116,754✔
347

348
                if ($$.isBarType(id) && removeNull) {
116,754✔
349
                        const ind = {} as IDataIndice;
54✔
350

351
                        // redefine bar indices order
352
                        $$.getAllValuesOnIndex(index, true)
54✔
353
                                .forEach((v, i) => {
354
                                        ind[v.id] = i;
108✔
355
                                        ind.__max__ = i;
108✔
356
                                });
357

358
                        return ind;
54✔
359
                }
360

361
                return notEmpty(xs) ? indices[xs[id]] : indices as IDataIndice;
116,700✔
362
        },
363

364
        /**
365
         * Get indices max number
366
         * @param {object} indices Indices object
367
         * @returns {number} Max number
368
         * @private
369
         */
370
        getIndicesMax(indices: TIndices | IDataIndice): number {
371
                if (!notEmpty(this.config.data_xs)) {
7,551✔
372
                        return (indices as IDataIndice).__max__;
7,437✔
373
                }
374

375
                // if is multiple xs, return total sum of xs' __max__ value
376
                let total = 0;
114✔
377

378
                for (const key in indices) {
114✔
379
                        total += indices[key].__max__ || 0;
210✔
380
                }
381

382
                return total;
114✔
383
        },
384

385
        getShapeX(offset: IOffset, indices, isSub?: boolean): (d) => number {
386
                const $$ = this;
29,166✔
387
                const {config, scale} = $$;
29,166✔
388
                const currScale = isSub ? scale.subX : (scale.zoom || scale.x);
29,166✔
389
                const barOverlap = config.bar_overlap;
29,166✔
390
                const barPadding = config.bar_padding;
29,166✔
391
                const sum = (p, c) => p + c;
29,166✔
392

393
                // total shapes half width
394
                const halfWidth = isObjectType(offset) && (
29,166✔
395
                        offset._$total.length ? offset._$total.reduce(sum) / 2 : 0
285✔
396
                );
397

398
                // Pre-compute prefix sums to avoid O(n) slice+reduce on every bar datum
399
                const prefixSums: number[] = [];
29,166✔
400

401
                if (halfWidth && isObjectType(offset) && offset._$total.length) {
29,166✔
402
                        let acc = 0;
45✔
403

404
                        for (const v of offset._$total) {
45✔
405
                                acc += v;
99✔
406
                                prefixSums.push(acc);
99✔
407
                        }
408
                }
409

410
                return d => {
29,166✔
411
                        const ind = $$.getIndices(indices, d, "getShapeX");
32,679✔
412
                        const index = d.id in ind ? ind[d.id] : 0;
32,679✔
413
                        const targetsNum = (ind.__max__ || 0) + 1;
32,679✔
414
                        let x = 0;
32,679✔
415

416
                        if (notEmpty(d.x)) {
32,679!
417
                                const xPos = currScale(d.x, true);
32,679✔
418

419
                                if (halfWidth) {
32,679✔
420
                                        const offsetWidth = offset[d.id] || offset._$width;
171!
421

422
                                        x = barOverlap ? xPos - offsetWidth / 2 : xPos - offsetWidth +
171✔
423
                                                (prefixSums[index] ?? offset._$total.slice(0, index + 1).reduce(sum)) -
144!
424
                                                halfWidth;
425
                                } else {
426
                                        x = xPos - (isNumber(offset) ? offset : offset._$width) *
32,508✔
427
                                                        (targetsNum / 2 - (
428
                                                                barOverlap ? 1 : index
32,508!
429
                                                        ));
430
                                }
431
                        }
432

433
                        // adjust x position for bar.padding option
434
                        if (offset && x && targetsNum > 1 && barPadding) {
32,679✔
435
                                if (index) {
432✔
436
                                        x += barPadding * index;
216✔
437
                                }
438

439
                                if (targetsNum > 2) {
432!
440
                                        x -= (targetsNum - 1) * barPadding / 2;
×
441
                                } else if (targetsNum === 2) {
432!
442
                                        x -= barPadding / 2;
432✔
443
                                }
444
                        }
445

446
                        return x;
32,679✔
447
                };
448
        },
449

450
        getShapeY(isSub?: boolean): Function {
451
                const $$ = this;
29,166✔
452
                const isStackNormalized = $$.isStackNormalized();
29,166✔
453

454
                return d => {
29,166✔
455
                        let {value} = d;
33,750✔
456

457
                        if (isNumber(d)) {
33,750✔
458
                                value = d;
1,428✔
459
                        } else if ($$.isAreaRangeType(d)) {
32,322✔
460
                                value = $$.getBaseValue(d, "mid");
228✔
461
                        } else if (isStackNormalized) {
32,094✔
462
                                value = $$.getRatio("index", d, true);
420✔
463
                        } else if ($$.isBubbleZType(d)) {
31,674!
464
                                value = $$.getBubbleZData(d.value, "y");
×
465
                        } else if ($$.isBarRangeType(d)) {
31,674✔
466
                                // TODO use range.getEnd() like method
467
                                value = value[1];
186✔
468
                        }
469

470
                        return $$.getYScaleById(d.id, isSub)(value);
33,750✔
471
                };
472
        },
473

474
        /**
475
         * Get shape based y Axis min value
476
         * @param {string} id Data id
477
         * @returns {number}
478
         * @private
479
         */
480
        getShapeYMin(id: string): number {
481
                const $$ = this;
72,120✔
482
                const axisId = $$.axis.getId(id);
72,120✔
483
                const scale = $$.scale[axisId];
72,120✔
484
                const [yMin] = scale.domain();
72,120✔
485
                const inverted = $$.config[`axis_${axisId}_inverted`];
72,120✔
486

487
                return !$$.isGrouped(id) && !inverted && yMin > 0 ? yMin : 0;
72,120✔
488
        },
489

490
        /**
491
         * Get Shape's offset data
492
         * @param {function} typeFilter Type filter function
493
         * @returns {object}
494
         * @private
495
         */
496
        getShapeOffsetData(typeFilter) {
497
                const $$ = this;
29,166✔
498
                const targets = $$.orderTargets(
29,166✔
499
                        $$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$))
500
                );
501

502
                // Create cache key based on target IDs
503
                const targetIds = targets.map(t => t.id).join("_");
51,429✔
504
                const cacheKey = `${KEY.shapeOffset}_${targetIds}`;
29,166✔
505

506
                // Check if result is already cached
507
                const cachedData = $$.cache.get(cacheKey);
29,166✔
508

509
                if (cachedData) {
29,166✔
510
                        return cachedData;
22,215✔
511
                }
512

513
                const isStackNormalized = $$.isStackNormalized();
6,951✔
514

515
                const shapeOffsetTargets = targets.map(target => {
6,951✔
516
                        let rowValues = target.values;
12,147✔
517
                        const values = {};
12,147✔
518

519
                        if ($$.isStepType(target)) {
12,147✔
520
                                rowValues = $$.convertValuesToStep(rowValues);
258✔
521
                        }
522

523
                        const rowValueMapByXValue = rowValues.reduce((out, d) => {
12,147✔
524
                                const key = Number(d.x);
69,072✔
525
                                const value = getShapeOffsetValue($$, d);
69,072✔
526

527
                                out[key] = d;
69,072✔
528
                                values[key] = isStackNormalized ? $$.getRatio("index", d, true) : value;
69,072✔
529

530
                                return out;
69,072✔
531
                        }, {});
532

533
                        return {
12,147✔
534
                                id: target.id,
535
                                rowValues,
536
                                rowValueMapByXValue,
537
                                values
538
                        };
539
                });
540
                const indexMapByTargetId = targets.reduce((out, {id}, index) => {
6,951✔
541
                        out[id] = index;
12,147✔
542
                        return out;
12,147✔
543
                }, {});
544

545
                const result = {indexMapByTargetId, shapeOffsetTargets};
6,951✔
546

547
                // Cache the result
548
                $$.cache.add(cacheKey, result);
6,951✔
549

550
                return result;
6,951✔
551
        },
552

553
        getShapeOffset(typeFilter, indices, isSub?: boolean): Function {
554
                const $$ = this;
29,166✔
555
                const {shapeOffsetTargets, indexMapByTargetId} = $$.getShapeOffsetData(
29,166✔
556
                        typeFilter
557
                );
558
                const groupsZeroAs = $$.config.data_groupsZeroAs;
29,166✔
559

560
                // Pre-build per-series same-stacking-group lookup to avoid .filter() on every datum.
561
                // bar_indices_removeNull recomputes group membership per-datum index, so fall back there.
562
                let sameGroupByTargetId: Map<string, typeof shapeOffsetTargets> | null = null;
29,166✔
563

564
                if (!$$.config.bar_indices_removeNull) {
29,166✔
565
                        sameGroupByTargetId = new Map();
29,157✔
566

567
                        for (const target of shapeOffsetTargets) {
29,157✔
568
                                const ind = $$.getIndices(indices, {id: target.id, index: 0} as IDataRow);
51,402✔
569

570
                                sameGroupByTargetId.set(
51,402✔
571
                                        target.id,
572
                                        shapeOffsetTargets.filter(
573
                                                t => t.id !== target.id && ind[t.id] === ind[target.id]
421,614✔
574
                                        )
575
                                );
576
                        }
577
                }
578

579
                return (d, idx) => {
29,166✔
580
                        const {id, value, x} = d;
32,682✔
581
                        const baseValue = getShapeOffsetValue($$, d);
32,682✔
582
                        const ind = $$.getIndices(indices, d);
32,682✔
583
                        const scale = $$.getYScaleById(id, isSub);
32,682✔
584

585
                        if ($$.isBarRangeType(d)) {
32,682✔
586
                                // TODO use range.getStart()
587
                                return scale(value[0]);
186✔
588
                        }
589

590
                        const dataXAsNumber = Number(x);
32,496✔
591
                        const y0 = scale(groupsZeroAs === "zero" ? 0 : $$.getShapeYMin(id));
32,496✔
592
                        let offset = y0;
32,496✔
593

594
                        const sameGroupTargets = sameGroupByTargetId?.get(id) ??
32,496✔
595
                                shapeOffsetTargets.filter(t => t.id !== id && ind[t.id] === ind[id]);
369✔
596

597
                        for (const t of sameGroupTargets) {
32,496✔
598
                                const {
599
                                        id: tid,
600
                                        rowValueMapByXValue,
601
                                        rowValues,
602
                                        values: tvalues
603
                                } = t;
49,950✔
604

605
                                // for same stacked group (ind[tid] === ind[id])
606
                                if (indexMapByTargetId[tid] < indexMapByTargetId[id]) {
49,950✔
607
                                        const rValue = tvalues[dataXAsNumber];
25,707✔
608
                                        let row = rowValues[idx];
25,707✔
609

610
                                        // check if the x values line up
611
                                        if (!row || Number(row.x) !== dataXAsNumber) {
25,707✔
612
                                                row = rowValueMapByXValue[dataXAsNumber];
909✔
613
                                        }
614

615
                                        const rowValue = row && getShapeOffsetValue($$, row);
25,707✔
616

617
                                        if (
25,707✔
618
                                                isNumber(rowValue) &&
67,536✔
619
                                                isNumber(baseValue) &&
620
                                                rowValue * baseValue >= 0 &&
621
                                                isNumber(rValue)
622
                                        ) {
623
                                                const addOffset = baseValue === 0 ?
13,431✔
624
                                                        (
625
                                                                (groupsZeroAs === "positive" &&
840✔
626
                                                                        rValue > 0) ||
627
                                                                (groupsZeroAs === "negative" && rValue < 0)
628
                                                        ) :
629
                                                        true;
630

631
                                                if (addOffset) {
13,431✔
632
                                                        offset += scale(rValue) - y0;
13,269✔
633
                                                }
634
                                        }
635
                                }
636
                        }
637

638
                        return offset;
32,496✔
639
                };
640
        },
641

642
        /**
643
         * Generate line coordinate points from shared geometry.
644
         * @param {object} lineIndices Data order within x axis
645
         * @param {boolean} isSub Whether the coordinates are for subchart
646
         * @param {function} typeFilter Type filter for offset targets
647
         * @returns {function} Line point generator
648
         * @private
649
         */
650
        generateGetLinePoints(lineIndices, isSub?: boolean, typeFilter?: Function): Function {
651
                const $$ = this;
19,608✔
652
                const {config} = $$;
19,608✔
653
                const x = $$.getShapeX(0, lineIndices, isSub);
19,608✔
654
                const y = $$.getShapeY(isSub);
19,608✔
655
                const lineOffset = $$.getShapeOffset(typeFilter || $$.isLineType, lineIndices, isSub);
19,608✔
656
                const yScale = $$.getYScaleById.bind($$);
19,608✔
657

658
                return (d, i) => {
19,608✔
659
                        const y0 = yScale.call($$, d.id, isSub)($$.getShapeYMin(d.id));
9,141✔
660
                        const offset = lineOffset(d, i) || y0;
9,141✔
661
                        const posX = x(d);
9,141✔
662
                        let posY = y(d);
9,141✔
663

664
                        if (
9,141!
665
                                config.axis_rotated && (
11,661✔
666
                                        (d.value > 0 && posY < y0) || (d.value < 0 && y0 < posY)
667
                                )
668
                        ) {
NEW
669
                                posY = y0;
×
670
                        }
671

672
                        const point = [posX, posY - (y0 - offset)];
9,141✔
673

674
                        return [
9,141✔
675
                                point,
676
                                point,
677
                                point,
678
                                point
679
                        ];
680
                };
681
        },
682

683
        /**
684
         * Generate area coordinate points from shared geometry.
685
         * @param {object} areaIndices Data order within x axis
686
         * @param {boolean} isSub Whether the coordinates are for subchart
687
         * @returns {function} Area point generator
688
         * @private
689
         */
690
        generateGetAreaPoints(
691
                areaIndices: TIndices,
692
                isSub?: boolean
693
        ): (d: IDataRow, i: number) => [number, number][] {
694
                const $$ = this;
2,010✔
695
                const {config} = $$;
2,010✔
696
                const x = $$.getShapeX(0, areaIndices, isSub);
2,010✔
697
                const y = $$.getShapeY(!!isSub);
2,010✔
698
                const areaOffset = $$.getShapeOffset($$.isAreaType, areaIndices, isSub);
2,010✔
699
                const yScale = $$.getYScaleById.bind($$);
2,010✔
700

701
                return function(d, i) {
2,010✔
702
                        const y0 = yScale.call($$, d.id, isSub)($$.getShapeYMin(d.id));
2,748✔
703
                        const offset = areaOffset(d, i) || y0;
2,748✔
704
                        const posX = x(d);
2,748✔
705
                        const value = d.value as number;
2,748✔
706
                        let posY = y(d);
2,748✔
707

708
                        if (
2,748!
709
                                config.axis_rotated && (
3,108!
710
                                        (value > 0 && posY < y0) || (value < 0 && y0 < posY)
711
                                )
712
                        ) {
NEW
713
                                posY = y0;
×
714
                        }
715

716
                        return [
2,748✔
717
                                [posX, offset],
718
                                [posX, posY - (y0 - offset)],
719
                                [posX, posY - (y0 - offset)],
720
                                [posX, offset]
721
                        ];
722
                };
723
        },
724

725
        /**
726
         * Generate bar coordinate points from shared geometry.
727
         * @param {object} barIndices Data order within x axis
728
         * @param {boolean} isSub Whether the coordinates are for subchart
729
         * @returns {function} Bar point generator
730
         * @private
731
         */
732
        generateGetBarPoints(
733
                barIndices,
734
                isSub?: boolean
735
        ): (d, i: number) => [number, number][] {
736
                const $$ = this;
6,312✔
737
                const {config} = $$;
6,312✔
738
                const axis = isSub ? $$.axis.subX : $$.axis.x;
6,312✔
739
                const barTargetsNum = $$.getIndicesMax(barIndices) + 1;
6,312✔
740
                const barW: IOffset = $$.getBarW("bar", axis, barTargetsNum);
6,312✔
741
                const barX = $$.getShapeX(barW, barIndices, !!isSub);
6,312✔
742
                const barY = $$.getShapeY(!!isSub);
6,312✔
743
                const barOffset = $$.getShapeOffset($$.isBarType, barIndices, !!isSub);
6,312✔
744
                const yScale = $$.getYScaleById.bind($$);
6,312✔
745

746
                return (d, i) => {
6,312✔
747
                        const {id} = d;
20,433✔
748
                        const y0 = yScale.call($$, id, isSub)($$.getShapeYMin(id));
20,433✔
749
                        const offset = barOffset(d, i) || y0;
20,433✔
750
                        const width = isNumber(barW) ? barW : barW[d.id] || barW._$width;
20,433✔
751
                        const isInverted = config[`axis_${$$.axis.getId(id)}_inverted`];
20,433✔
752
                        const value = d.value as number;
20,433✔
753
                        const posX = barX(d);
20,433✔
754
                        let posY = barY(d);
20,433✔
755

756
                        if (
20,433!
757
                                config.axis_rotated && !isInverted && (
37,641✔
758
                                        (value > 0 && posY < y0) || (value < 0 && y0 < posY)
759
                                )
760
                        ) {
NEW
761
                                posY = y0;
×
762
                        }
763

764
                        if (!$$.isBarRangeType(d)) {
20,433✔
765
                                posY -= y0 - offset;
20,247✔
766
                        }
767

768
                        const startPosX = posX + width;
20,433✔
769

770
                        return [
20,433✔
771
                                [posX, offset],
772
                                [posX, posY],
773
                                [startPosX, posY],
774
                                [startPosX, offset]
775
                        ];
776
                };
777
        },
778

779
        /**
780
         * Get data's y coordinate
781
         * @param {object} d Target data
782
         * @param {number} i Index number
783
         * @returns {number} y coordinate
784
         * @private
785
         */
786
        circleY(d: IDataRow, i: number): number {
787
                const $$ = this;
684,503✔
788
                const id = d.id;
684,503✔
789
                let points;
790

791
                if ($$.isGrouped(id)) {
684,503✔
792
                        points = _getGroupedDataPointsFn.bind($$)(d);
1,776✔
793
                }
794

795
                return points ? points(d, i)[0][1] : $$.getYScaleById(id)($$.getBaseValue(d));
684,503✔
796
        },
797

798
        /**
799
         * Get data point x coordinate.
800
         * @param {object} d Data row
801
         * @returns {number|null} X coordinate
802
         * @private
803
         */
804
        circleX(d): number | null {
805
                return this.xx(d);
683,663✔
806
        },
807

808
        /**
809
         * Generate data point y coordinate accessor.
810
         * @param {boolean} isSub Whether the coordinates are for subchart
811
         * @returns {function} Y coordinate accessor
812
         * @private
813
         */
814
        updateCircleY(isSub = false): Function {
×
815
                const $$ = this;
123✔
816
                const typeFilter = getLinePointGroupTypeFilter($$);
123✔
817
                const getPoints = $$.generateGetLinePoints($$.getShapeIndices(typeFilter), isSub,
123✔
818
                        typeFilter);
819

820
                return (d, i) => {
123✔
821
                        const id = d.id;
840✔
822

823
                        return $$.isGrouped(id) && isLinePointGroupType($$, d) ?
840✔
824
                                getPoints(d, i)[0][1] :
825
                                $$.getYScaleById(id, isSub)($$.getBaseValue(d));
826
                };
827
        },
828

829
        /**
830
         * Get point radius.
831
         * @param {object} d Data row
832
         * @returns {number} Point radius
833
         * @private
834
         */
835
        pointR(d): number {
836
                const $$ = this;
45,123✔
837
                const {config} = $$;
45,123✔
838
                const pointR = config.point_r;
45,123✔
839
                let r = pointR;
45,123✔
840

841
                if ($$.isBubbleType(d)) {
45,123✔
842
                        r = $$.getBubbleR(d);
2,202✔
843
                } else if (isFunction(pointR)) {
42,921!
NEW
844
                        r = pointR.bind($$.api)(d);
×
845
                }
846

847
                d.r = r;
45,123✔
848

849
                return r;
45,123✔
850
        },
851

852
        /**
853
         * Get focused point radius.
854
         * @param {object} d Data row
855
         * @returns {number} Focused point radius
856
         * @private
857
         */
858
        pointExpandedR(d): number {
859
                const $$ = this;
2,025✔
860
                const {config} = $$;
2,025✔
861
                const scale = $$.isBubbleType(d) ? 1.15 : 1.75;
2,025✔
862

863
                return config.point_focus_expand_enabled ?
2,025!
864
                        (config.point_focus_expand_r || $$.pointR(d) * scale) :
4,032✔
865
                        $$.pointR(d);
866
        },
867

868
        /**
869
         * Get selected point radius.
870
         * @param {object} d Data row
871
         * @returns {number} Selected point radius
872
         * @private
873
         */
874
        pointSelectR(d): number {
875
                const $$ = this;
231✔
876
                const selectR = $$.config.point_select_r;
231✔
877

878
                return isFunction(selectR) ? selectR(d) : (selectR || $$.pointR(d) * 4);
231!
879
        },
880

881
        /**
882
         * Check if point.focus.only option can be applied.
883
         * @returns {boolean} Whether focus-only point rendering is active
884
         * @private
885
         */
886
        isPointFocusOnly(): boolean {
887
                const $$ = this;
65,535✔
888

889
                return $$.config.point_focus_only &&
65,535✔
890
                        !$$.hasType("bubble") && !$$.hasType("scatter") && !$$.hasArcType(null, ["radar"]);
891
        },
892

893
        /**
894
         * Get data point sensitivity radius.
895
         * @param {object} d Data point
896
         * @returns {number} Sensitivity radius
897
         * @private
898
         */
899
        getPointSensitivity(d) {
900
                const $$ = this;
1,530✔
901
                let sensitivity = $$.config.point_sensitivity;
1,530✔
902

903
                if (!d) {
1,530✔
904
                        return sensitivity;
6✔
905
                } else if (isFunction(sensitivity)) {
1,524✔
906
                        sensitivity = sensitivity.call($$.api, d);
60✔
907
                } else if (sensitivity === "radius") {
1,464✔
908
                        sensitivity = d.r;
108✔
909
                }
910

911
                return sensitivity;
1,524✔
912
        },
913

914
        getBarW(type, axis, targetsNum: number): number | IOffset {
915
                const $$ = this;
7,548✔
916
                const {config, org, scale, state} = $$;
7,548✔
917
                const maxDataCount = $$.getMaxDataCount();
7,548✔
918
                const isGrouped = type === "bar" && config.data_groups?.length;
7,548✔
919
                const configName = `${type}_width`;
7,548✔
920
                const {k} = $$.getZoomTransform?.() ?? {k: 1};
7,548✔
921
                const xMinMax = [
7,548✔
922
                        config.axis_x_min ?? org.xDomain[0],
14,991✔
923
                        config.axis_x_max ?? org.xDomain[1]
15,018✔
924
                ].map(v => ($$.axis.isTimeSeries() ? parseDate.call($$, v) : Number(v))) as [
15,096✔
925
                        number,
926
                        number
927
                ];
928

929
                let tickInterval = axis.tickInterval(maxDataCount);
7,548✔
930

931
                if (scale.zoom && !$$.axis.isCategorized() && k > 1) {
7,548✔
932
                        const isSameMinMax = xMinMax.every((v, i) => v === org.xDomain[i]);
282✔
933

934
                        tickInterval = org.xDomain.map((v, i) => {
150✔
935
                                const value = isSameMinMax ? v : v - Math.abs(xMinMax[i]);
300✔
936

937
                                return scale.zoom(value);
300✔
938
                        }).reduce((a, c) => Math.abs(a) + c) / maxDataCount;
150✔
939
                }
940

941
                const getWidth = (id?: string) => {
7,548✔
942
                        const width = id ? config[configName][id] : config[configName];
7,647✔
943
                        const ratio = id ? width.ratio : config[`${configName}_ratio`];
7,647✔
944
                        const max = id ? width.max : config[`${configName}_max`];
7,647✔
945
                        const w = isNumber(width) ? width : (
7,647✔
946
                                isFunction(width) ?
7,413✔
947
                                        width.call($$, state.width, targetsNum, maxDataCount) :
948
                                        (targetsNum ? (tickInterval * ratio) / targetsNum : 0)
7,395✔
949
                        );
950

951
                        return max && w > max ? max : w;
7,647✔
952
                };
953

954
                let result = getWidth();
7,548✔
955

956
                if (!isGrouped && isObjectType(config[configName])) {
7,548✔
957
                        result = {_$width: result, _$total: []};
285✔
958

959
                        $$.getTargetsToShow().forEach(v => {
285✔
960
                                if (config[configName][v.id]) {
483✔
961
                                        result[v.id] = getWidth(v.id);
99✔
962
                                        result._$total.push(result[v.id] || result._$width);
99!
963
                                }
964
                        });
965
                }
966

967
                return result;
7,548✔
968
        },
969

970
        /**
971
         * Get shape element
972
         * @param {string} shapeName Shape string
973
         * @param {number} i Index number
974
         * @param {string} id Data series id
975
         * @returns {d3Selection}
976
         * @private
977
         */
978
        getShapeByIndex(shapeName: string, i: number, id?: string): d3Selection {
979
                const $$ = this;
2,013✔
980
                const {$el} = $$;
2,013✔
981
                const suffix = isValue(i) ? `-${i}` : ``;
2,013✔
982
                let shape = $el[shapeName];
2,013✔
983

984
                // filter from shape reference if has
985
                if (shape && !shape.empty()) {
2,013✔
986
                        shape = shape
1,938✔
987
                                .filter(d => (id ? d.id === id : true))
18,624✔
988
                                .filter(d => (isValue(i) ? d.index === i : true));
17,517✔
989
                } else {
990
                        shape = (id ?
75!
991
                                $el.main
992
                                        .selectAll(
993
                                                `.${CLASS[`${shapeName}s`]}${$$.getTargetSelectorSuffix(id)}`
994
                                        ) :
995
                                $el.main)
996
                                .selectAll(`.${CLASS[shapeName]}${suffix}`);
997
                }
998

999
                return shape;
2,013✔
1000
        },
1001

1002
        isWithinShape(that, d): boolean {
1003
                const $$ = this;
663✔
1004
                const shape = d3Select(that);
663✔
1005
                let isWithin;
1006

1007
                if (!$$.isTargetToShow(d.id)) {
663!
1008
                        isWithin = false;
×
1009
                } else if ($$.hasValidPointType?.(that.nodeName)) {
663✔
1010
                        isWithin = $$.isStepType(d) ?
441!
1011
                                $$.isWithinStep(that, $$.getYScaleById(d.id)($$.getBaseValue(d))) :
1012
                                $$.isWithinCircle(
1013
                                        that,
1014
                                        $$.isBubbleType(d) ? $$.pointSelectR(d) * 1.5 : 0
441✔
1015
                                );
1016
                } else if (that.nodeName === "path") {
222!
1017
                        isWithin = shape.classed(CLASS.bar) ? $$.isWithinBar(that) : true;
222!
1018
                }
1019

1020
                return isWithin;
663✔
1021
        },
1022

1023
        getInterpolate(d) {
1024
                const $$ = this;
10,986✔
1025
                const interpolation = $$.getInterpolateType(d);
10,986✔
1026

1027
                return CURVE_MAP[interpolation];
10,986✔
1028
        },
1029

1030
        /**
1031
         * Get curve generator for line-like shapes.
1032
         * @param {object} d Data target
1033
         * @returns {function} Curve generator
1034
         * @private
1035
         */
1036
        getCurve(d): Function {
1037
                const $$ = this;
10,980✔
1038
                const isRotatedStepType = $$.config.axis_rotated && $$.isStepType(d);
10,980✔
1039

1040
                return isRotatedStepType ?
10,980✔
1041
                        context => {
1042
                                const step = $$.getInterpolate(d)(context);
30✔
1043

1044
                                step.orgPoint = step.point;
30✔
1045

1046
                                step.pointRotated = function(x, y) {
30✔
1047
                                        this._point === 1 && (this._point = 2);
342✔
1048

1049
                                        const y1 = this._y * (1 - this._t) + y * this._t;
342✔
1050

1051
                                        this._context.lineTo(this._x, y1);
342✔
1052
                                        this._context.lineTo(x, y1);
342✔
1053

1054
                                        this._x = x;
342✔
1055
                                        this._y = y;
342✔
1056
                                };
1057

1058
                                step.point = function(x, y) {
30✔
1059
                                        this._point === 0 ? this.orgPoint(x, y) : this.pointRotated(x, y);
387✔
1060
                                };
1061

1062
                                return step;
30✔
1063
                        } :
1064
                        $$.getInterpolate(d);
1065
        },
1066

1067
        getInterpolateType(d) {
1068
                const $$ = this;
10,998✔
1069
                const {config} = $$;
10,998✔
1070
                const type = config.spline_interpolation_type;
10,998✔
1071
                const interpolation = $$.isInterpolationType(type) ? type : "cardinal";
10,998✔
1072

1073
                return $$.isSplineType(d) ? interpolation : (
10,998✔
1074
                        $$.isStepType(d) ? config.line_step_type : "linear"
10,389✔
1075
                );
1076
        },
1077

1078
        isWithinBar(that): boolean {
1079
                const mouse = getPointer(this.state.event, that);
273✔
1080
                const list = getRectSegList(that);
273✔
1081
                const [seg0, seg1, seg2] = list;
273✔
1082
                const x = Math.min(seg0.x, seg1.x);
273✔
1083
                const y = Math.min(seg0.y, seg1.y);
273✔
1084
                const offset = this.config.bar_sensitivity;
273✔
1085
                const width = Math.abs(seg2.x - seg1.x);
273✔
1086
                const height = Math.abs(seg0.y - seg1.y);
273✔
1087
                const sx = x - offset;
273✔
1088
                const ex = x + width + offset;
273✔
1089
                const sy = y + height + offset;
273✔
1090
                const ey = y - offset;
273✔
1091

1092
                const isWithin = sx < mouse[0] &&
273✔
1093
                        mouse[0] < ex &&
1094
                        ey < mouse[1] &&
1095
                        mouse[1] < sy;
1096

1097
                return isWithin;
273✔
1098
        }
1099
};
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