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

naver / billboard.js / 30443109728

29 Jul 2026 10:17AM UTC coverage: 93.712% (-0.1%) from 93.823%
30443109728

push

github

web-flow
feat(subchart): support configurable subchart rendering

Add subchart.type and subchart.types for independent overview series rendering.
Support subchart x/y/y2 axis ticks, formats, culling, outer ticks, and text visibility.
Add brush.enabled=false hover behavior and continuous focus grid across main/subchart.
Project candlestick values for subchart bars, line, and area using stock chart semantics.
Align SVG/canvas subchart axis, grid, and interaction behavior with demos/tests.

Ref #4175

11530 of 12842 branches covered (89.78%)

Branch coverage included in aggregate %.

553 of 584 new or added lines in 20 files covered. (94.69%)

108 existing lines in 12 files now uncovered.

14417 of 14846 relevant lines covered (97.11%)

29561.24 hits per line

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

95.03
/src/Chart/api/selection.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 type {DataItem} from "../../../types/types";
7
import {$AREA, $LINE, $SELECT, $SHAPE} from "../../config/classes";
8
import {isDefined} from "../../module/util";
9

10
/**
11
 * Toggler function to select or unselect when point.focus.only=true.<br><br>
12
 * In this mode only a single shared circle element is rendered per series, so
13
 * selection can't rely on per-index shape elements existing in the DOM. Iterate
14
 * over the data instead and draw/remove the selected-circle elements directly.
15
 * @param {boolean} isSelection Weather select or unselect
16
 * @param {Array} ids Target ids
17
 * @param {Array} indices Indices number
18
 * @param {boolean} resetOther Weather reset other selected points (only for selection)
19
 * @private
20
 */
21
function setSelectionForFocusOnly(
22
        isSelection: boolean,
23
        ids?: string | string[],
24
        indices?: number[],
25
        resetOther?: boolean
26
): void {
27
        const $$ = this;
60✔
28
        const {config, $el: {main}} = $$;
60✔
29
        const selectionGrouped = config.data_selection_grouped;
60✔
30
        const isSelectable = config.data_selection_isselectable.bind($$.api);
60✔
31
        const targetIds = isDefined(ids) ? ([] as string[]).concat(ids as string | string[]) : null;
60✔
32
        const singleSelection = isSelection && !config.data_selection_multiple;
60✔
33
        let resetDone = !singleSelection;
60✔
34

35
        // Remove the selected-circle synchronously. The shape's selection state is
36
        // represented solely by these elements in focus.only mode, so relying on
37
        // unselectPoint's transitioned removal would let rapid API calls interrupt
38
        // each other's transition and leave orphaned circles behind.
39
        const unselect = (circle, d, index) => {
60✔
40
                $$.unselectPoint(circle, d, index);
15✔
41
                circle.interrupt().remove();
15✔
42
        };
43

44
        $$.getTargetsToShow().forEach(target => {
60✔
45
                const {id} = target;
60✔
46
                const isTargetId = selectionGrouped || !targetIds || targetIds.indexOf(id) >= 0;
60!
47
                const selectedCircles = main.select(
60✔
48
                        `.${$SELECT.selectedCircles}${$$.getTargetSelectorSuffix(id)}`
49
                );
50

51
                target.values.forEach(d => {
60✔
52
                        const {index} = d;
360✔
53
                        const isTargetIndex = !indices || indices.indexOf(index) >= 0;
360✔
54
                        const circle = selectedCircles.selectAll(`.${$SELECT.selectedCircle}-${index}`);
360✔
55
                        const isSelected = !circle.empty();
360✔
56

57
                        if (isSelection) {
360✔
58
                                if (
180✔
59
                                        isTargetId && isTargetIndex && isSelectable(d) &&
414!
60
                                        (!isSelected || singleSelection)
61
                                ) {
62
                                        if (!resetDone) {
27!
63
                                                setSelectionForFocusOnly.call($$, false);
27✔
64
                                                resetDone = true;
27✔
65
                                        }
66

67
                                        $$.selectPoint(null, d, index);
27✔
68
                                } else if (
153!
69
                                        (!singleSelection || resetDone) &&
70
                                        isDefined(resetOther) &&
71
                                        resetOther &&
72
                                        isSelected
73
                                ) {
UNCOV
74
                                        unselect(circle, d, index);
×
75
                                }
76
                        } else if (isTargetId && isTargetIndex && isSelected) {
180✔
77
                                unselect(circle, d, index);
15✔
78
                        }
79
                });
80
        });
81
}
82

83
/**
84
 * Toggler function to select or unselect
85
 * @param {boolean} isSelection Weather select or unselect
86
 * @param {Array} ids Target ids
87
 * @param {Array} indices Indices number
88
 * @param {boolean} resetOther Weather reset other selected points (only for selection)
89
 * @private
90
 */
91
function setSelection(
92
        isSelection = false,
×
93
        ids?: string | string[],
94
        indices?: number[],
95
        resetOther?: boolean
96
): void {
97
        const $$ = this;
114✔
98
        const {config, $el: {main}} = $$;
114✔
99
        const selectionGrouped = config.data_selection_grouped;
114✔
100
        const isSelectable = config.data_selection_isselectable.bind($$.api);
114✔
101
        const singleSelection = isSelection && !config.data_selection_multiple;
114✔
102
        let resetDone = !singleSelection;
114✔
103

104
        if (!config.data_selection_enabled) {
114✔
105
                return;
3✔
106
        }
107

108
        // When multiple selection is disabled, only one data point (or one x-index
109
        // group when 'grouped' is enabled) can hold the selected state at a time.
110
        // Clear any current selection only when the narrowed request can select a point.
111
        if (singleSelection) {
111✔
112
                indices = indices?.length ? [indices[0]] : [0];
54✔
113

114
                // non-grouped single selection targets exactly one point, so narrow to a
115
                // single id (the first requested, or the first shown when none given).
116
                if (!selectionGrouped) {
54✔
117
                        const targetIds = isDefined(ids) ?
18✔
118
                                ([] as string[]).concat(ids as string | string[]) :
119
                                $$.getTargetsToShow().map(t => t.id);
6✔
120

121
                        ids = targetIds.slice(0, 1);
18✔
122
                }
123
        }
124

125
        if ($$.isPointFocusOnly?.()) {
111✔
126
                setSelectionForFocusOnly.call($$, isSelection, ids, indices, resetOther);
33✔
127
                return;
33✔
128
        }
129

130
        main.selectAll(`.${$SHAPE.shapes}`)
78✔
131
                .selectAll(`.${$SHAPE.shape}`)
132
                .each(function(d) {
133
                        const shape = d3Select(this);
1,029✔
134
                        const {id, index} = d.data ? d.data : d;
1,029!
135
                        const isTargetId = selectionGrouped || !ids || ids.indexOf(id) >= 0;
1,029✔
136
                        const isTargetIndex = !indices || indices.indexOf(index) >= 0;
1,029✔
137

138
                        // line/area selection not supported yet
139
                        if (shape.classed($LINE.line) || shape.classed($AREA.area)) {
1,029✔
140
                                return;
135✔
141
                        }
142

143
                        const toggle = $$.getToggle(this, d).bind($$);
894✔
144
                        const isSelected = shape.classed($SELECT.SELECTED);
894✔
145

146
                        if (isSelection) {
894✔
147
                                if (
522✔
148
                                        isTargetId &&
1,059✔
149
                                        isTargetIndex &&
150
                                        isSelectable(d) &&
151
                                        (!isSelected || singleSelection)
152
                                ) {
153
                                        if (!resetDone) {
105✔
154
                                                setSelection.call($$, false);
21✔
155
                                                resetDone = true;
21✔
156
                                        }
157

158
                                        !shape.classed($SELECT.SELECTED) &&
105✔
159
                                                toggle(true, shape.classed($SELECT.SELECTED, true), d, index);
160
                                } else if (
417✔
161
                                        (!singleSelection || resetDone) &&
162
                                        isDefined(resetOther) &&
163
                                        resetOther &&
164
                                        isSelected
165
                                ) {
166
                                        toggle(false, shape.classed($SELECT.SELECTED, false), d, index);
30✔
167
                                }
168
                        } else {
169
                                if (isTargetId && isTargetIndex && isSelectable(d) && isSelected) {
372✔
170
                                        toggle(false, shape.classed($SELECT.SELECTED, false), d, index);
48✔
171
                                }
172
                        }
173
                });
174
}
175

176
export default {
177
        /**
178
         * Get selected data points.<br><br>
179
         * By this API, you can get selected data points information. To use this API, data.selection.enabled needs to be set true.
180
         * @function selected
181
         * @instance
182
         * @memberof Chart
183
         * @param {string} [targetId] You can filter the result by giving target id that you want to get. If not given, all of data points will be returned.
184
         * @returns {Array} dataPoint Array of the data points.<br>ex.) `[{x: 1, value: 200, id: "data1", index: 1, name: "data1"}, ...]`
185
         * @example
186
         *  // all selected data points will be returned.
187
         *  chart.selected();
188
         *  // --> ex.) [{x: 1, value: 200, id: "data1", index: 1, name: "data1"}, ... ]
189
         *
190
         *  // all selected data points of data1 will be returned.
191
         *  chart.selected("data1");
192
         */
193
        selected(targetId?: string): DataItem[] {
194
                const $$ = this.internal;
96✔
195
                const dataPoint: DataItem[] = [];
96✔
196

197
                if ($$.state.isCanvasMode) {
96✔
198
                        return $$.getCanvasSelectedData?.(targetId) || dataPoint;
57!
199
                }
200

201
                // point.focus.only=true renders no per-index shape, so the selected-circle
202
                // elements are the source of truth for the selection state.
203
                if ($$.isPointFocusOnly?.()) {
39✔
204
                        $$.$el.main
18✔
205
                                .selectAll(`.${$SELECT.selectedCircles + $$.getTargetSelectorSuffix(targetId)}`)
206
                                .selectAll(`.${$SELECT.selectedCircle}`)
207
                                .each(d => dataPoint.push(d));
15✔
208

209
                        return dataPoint;
18✔
210
                }
211

212
                $$.$el.main.selectAll(`.${$SHAPE.shapes + $$.getTargetSelectorSuffix(targetId)}`)
21✔
213
                        .selectAll(`.${$SHAPE.shape}`)
214
                        .filter(function() {
215
                                return d3Select(this).classed($SELECT.SELECTED);
276✔
216
                        })
217
                        .each(d => dataPoint.push(d));
27✔
218

219
                return dataPoint;
21✔
220
        },
221

222
        /**
223
         * Set data points to be selected. ([`data.selection.enabled`](Options.html#.data%25E2%2580%25A4selection%25E2%2580%25A4enabled) option should be set true to use this method)
224
         * @function select
225
         * @instance
226
         * @memberof Chart
227
         * @param {string|Array} [ids] id value to get selected.
228
         * @param {Array} [indices] The index array of data points. If falsy value given, will select all data points.
229
         * @param {boolean} [resetOther] Unselect already selected.
230
         * @example
231
         *  // select all data points
232
         *  chart.select();
233
         *
234
         *  // select all from 'data2'
235
         *  chart.select("data2");
236
         *
237
         *  // select all from 'data1' and 'data2'
238
         *  chart.select(["data1", "data2"]);
239
         *
240
         *  // select from 'data1', indices 2 and unselect others selected
241
         *  chart.select("data1", [2], true);
242
         *
243
         *  // select from 'data1', indices 0, 3 and 5
244
         *  chart.select("data1", [0, 3, 5]);
245
         */
246
        select(ids?: string[] | string, indices?: number[], resetOther?: boolean): void {
247
                const $$ = this.internal;
114✔
248

249
                if ($$.state.isCanvasMode) {
114✔
250
                        $$.setCanvasSelection?.(true, ids, indices, resetOther);
36✔
251
                        return;
36✔
252
                }
253

254
                setSelection.bind($$)(true, ids, indices, resetOther);
78✔
255
        },
256

257
        /**
258
         * Set data points to be un-selected.
259
         * @function unselect
260
         * @instance
261
         * @memberof Chart
262
         * @param {string|Array} [ids] id value to be unselected.
263
         * @param {Array} [indices] The index array of data points. If falsy value given, will select all data points.
264
         * @example
265
         *  // unselect all data points
266
         *  chart.unselect();
267
         *
268
         *  // unselect all from 'data1'
269
         *  chart.unselect("data1");
270
         *
271
         *  // unselect from 'data1', indices 2
272
         *  chart.unselect("data1", [2]);
273
         */
274
        unselect(ids?: string | string[], indices?: number[]): void {
275
                const $$ = this.internal;
33✔
276

277
                if ($$.state.isCanvasMode) {
33✔
278
                        $$.setCanvasSelection?.(false, ids, indices);
18✔
279
                        return;
18✔
280
                }
281

282
                setSelection.bind($$)(false, ids, indices);
15✔
283
        }
284
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc