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

naver / billboard.js / 27269607796

10 Jun 2026 10:20AM UTC coverage: 93.875% (+0.08%) from 93.792%
27269607796

push

github

web-flow
refactor(all): fix potential bugs & improve perf (#4139)

* refactor(all): fix potential bugs & improve perf

* fix(canvas): support public API parity

- Handle focus, defocus and revert without SVG targets in canvas mode.
- Keep legend focus state and canvas frames in sync for those APIs.
- Route tooltip.show and tooltip.hide through canvas focus rendering.
- Clear canvas focus state when programmatic tooltip APIs hide the tooltip.
- Use canvas subchart domain helpers for zoom and unzoom instead of SVG brush access.
- Add API canvas tests under test/api for core public APIs and canvas-only behavior.

* skip: fix build type error

10968 of 12185 branches covered (90.01%)

Branch coverage included in aggregate %.

314 of 325 new or added lines in 44 files covered. (96.62%)

3 existing lines in 3 files now uncovered.

13815 of 14215 relevant lines covered (97.19%)

27085.9 hits per line

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

92.76
/src/ChartInternal/data/load.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 */
5
import {$LEGEND} from "../../config/classes";
6
import {KEY} from "../../module/Cache";
7
import {endall} from "../../module/util";
8

9
/**
10
 * Call done callback with resize after transition
11
 * @param {function} fn Callback function
12
 * @param {boolean} resizeAfter Weather to resize chart after the load
13
 * @private
14
 */
15
export function callDone(fn, resizeAfter = false) {
282✔
16
        const $$ = this;
291✔
17
        const {api} = $$;
291✔
18

19
        resizeAfter && $$.api.flush(true);
291✔
20
        fn?.call(api);
291✔
21
}
22

23
export default {
24
        load(rawTargets, args): void {
25
                const $$ = this;
252✔
26
                const {axis, data, org, scale} = $$;
252✔
27
                const {append} = args;
252✔
28
                const zoomState = {
252✔
29
                        domain: <any>null,
30
                        currentDomain: <any>null,
31
                        x: <any>null
32
                };
33
                let targets = rawTargets;
252✔
34

35
                if (targets) {
252!
36
                        // filter loading targets if needed
37
                        if (args.filter) {
252!
38
                                targets = targets.filter(args.filter);
×
39
                        }
40

41
                        // set type if args.types || args.type specified
42
                        if (args.type || args.types) {
252✔
43
                                targets.forEach(t => {
18✔
44
                                        const type = args.types?.[t.id] || args.type;
21✔
45

46
                                        $$.setTargetType(t.id, type);
21✔
47
                                });
48
                        }
49

50
                        // Update/Add data: index incoming targets by id to avoid O(n×m) scans
51
                        const incoming = new Map<string, any>(targets.map(t => [t.id, t]));
399✔
52

53
                        data.targets.forEach(d => {
252✔
54
                                const t = incoming.get(d.id);
387✔
55

56
                                if (t) {
387✔
57
                                        if (append) {
177✔
58
                                                const values = t.values;
21✔
59

60
                                                for (let j = 0; j < values.length; j++) {
21✔
61
                                                        d.values.push(values[j]);
30✔
62
                                                }
63
                                        } else {
64
                                                d.values = t.values;
156✔
65
                                        }
66

67
                                        incoming.delete(d.id);
177✔
68
                                }
69
                        });
70

71
                        // add remained
72
                        incoming.forEach(t => data.targets.push(t));
252✔
73
                }
74

75
                if ($$.state.isCanvasMode) {
252✔
76
                        $$.redraw({
9✔
77
                                withUpdateOrgXDomain: true,
78
                                withUpdateXDomain: true,
79
                                withLegend: true
80
                        });
81
                        $$.updateTypesElements();
9✔
82

83
                        callDone.call($$, args.done, args.resizeAfter);
9✔
84
                        return;
9✔
85
                }
86

87
                // Set targets
88
                $$.updateTargets(data.targets);
243✔
89

90
                if (scale.zoom) {
243✔
91
                        zoomState.x = axis.isCategorized() ?
9✔
92
                                scale.x.orgScale() :
93
                                (org.xScale || scale.x).copy();
12✔
94
                        zoomState.domain = $$.getXDomain(data.targets); // get updated xDomain
9✔
95

96
                        zoomState.x.domain(zoomState.domain);
9✔
97
                        zoomState.currentDomain = $$.zoom.getDomain(); // current zoomed domain
9✔
98

99
                        // reset zoom state when new data loaded is out of range
100
                        if (!$$.withinRange(zoomState.currentDomain, undefined, zoomState.domain)) {
9!
101
                                scale.x.domain(zoomState.domain);
×
102
                                scale.zoom = null;
×
103
                                $$.$el.eventRect.property("__zoom", null);
×
104
                        }
105
                }
106

107
                // Redraw with new targets
108
                $$.redraw({
243✔
109
                        withUpdateOrgXDomain: true,
110
                        withUpdateXDomain: true,
111
                        withLegend: true
112
                });
113

114
                // when load happens on zoom state
115
                if (scale.zoom) {
243✔
116
                        // const x = (axis.isCategorized() ? scale.x.orgScale() : (org.xScale || scale.x)).copy();
117

118
                        org.xDomain = zoomState.domain;
9✔
119
                        org.xScale = zoomState.x;
9✔
120

121
                        if (axis.isCategorized()) {
9✔
122
                                zoomState.currentDomain = $$.getZoomDomainValue(zoomState.currentDomain);
3✔
123
                                org.xDomain = $$.getZoomDomainValue(org.xDomain);
3✔
124
                                org.xScale = zoomState.x.domain(org.xDomain);
3✔
125
                        }
126

127
                        $$.updateCurrentZoomTransform(zoomState.x, zoomState.currentDomain);
9✔
128

129
                        // https://github.com/naver/billboard.js/issues/3878
130
                } else if (org.xScale) {
234✔
131
                        org.xScale.domain(org.xDomain);
3✔
132
                }
133

134
                // Update current state chart type and elements list after redraw
135
                $$.updateTypesElements();
243✔
136

137
                callDone.call($$, args.done, args.resizeAfter);
243✔
138
        },
139

140
        loadFromArgs(args): void {
141
                const $$ = this;
252✔
142

143
                // prevent load when chart is already destroyed
144
                if (!$$.config) {
252!
UNCOV
145
                        return;
×
146
                }
147

148
                // Reset non-generation-based caches only.
149
                // Generation-based caches ($filteredTargets, $maxDataCountTarget, $valuesXIndexMap,
150
                // $maxTickSize_*) are invalidated automatically by dataGeneration/redrawGeneration
151
                // increments at the start of the next redraw — no need to delete them eagerly.
152
                $$.cache.reset(false, [
252✔
153
                        KEY.filteredTargets,
154
                        KEY.maxDataCountTarget,
155
                        KEY.valuesXIndexMap,
156
                        KEY.maxTickSize
157
                ]);
158

159
                $$.convertData(args, d => {
252✔
160
                        const data = args.data || d;
252✔
161

162
                        args.append && (data.__append__ = true);
252✔
163
                        data && $$.load($$.convertDataToTargets.call($$, data), args);
252✔
164
                });
165
        },
166

167
        unload(rawTargetIds, customDoneCb): void {
168
                const $$ = this;
75✔
169
                const {state, $el, $T} = $$;
75✔
170
                const hasLegendDefsPoint = !!$$.hasLegendDefsPoint?.();
75✔
171
                let done = customDoneCb;
75✔
172
                let targetIds = rawTargetIds;
75✔
173

174
                // Reset non-generation-based caches only (same rationale as loadFromArgs)
175
                $$.cache.reset(false, [
75✔
176
                        KEY.filteredTargets,
177
                        KEY.maxDataCountTarget,
178
                        KEY.valuesXIndexMap,
179
                        KEY.maxTickSize
180
                ]);
181

182
                if (!done) {
75!
183
                        done = () => {};
×
184
                }
185

186
                // filter existing target
187
                targetIds = targetIds.filter(id => $$.hasTarget($$.data.targets, id));
96✔
188

189
                // If no target, call done and return
190
                if (targetIds.length === 0) {
75✔
191
                        done();
6✔
192
                        return;
6✔
193
                }
194

195
                // remove in a single pass instead of re-filtering per id
196
                const unloadIds = new Set(targetIds);
69✔
197

198
                if (state.isCanvasMode) {
69✔
199
                        targetIds.forEach(id => {
6✔
200
                                state.withoutFadeIn[id] = false;
6✔
201
                        });
202

203
                        $$.data.targets = $$.data.targets.filter(t => !unloadIds.has(t.id));
18✔
204
                        $$.removeHiddenTargetIds(targetIds);
6✔
205
                        $$.removeHiddenLegendIds(targetIds);
6✔
206
                        $$.updateTypesElements();
6✔
207

208
                        done();
6✔
209
                        return;
6✔
210
                }
211

212
                targetIds.forEach(id => {
63✔
213
                        const suffixId = $$.getTargetSelectorSuffix(id);
87✔
214

215
                        // Reset fadein for future load
216
                        state.withoutFadeIn[id] = false;
87✔
217

218
                        // Remove target's elements
219
                        if ($el.legend) {
87✔
220
                                $el.legend.selectAll(`.${$LEGEND.legendItem}${suffixId}`).remove();
78✔
221
                        }
222

223
                        // Remove custom point def element
224
                        hasLegendDefsPoint && $el.defs?.select(`#${$$.getDefsPointId(suffixId)}`).remove();
87✔
225
                });
226

227
                // Remove targets
228
                $$.data.targets = $$.data.targets.filter(t => !unloadIds.has(t.id));
207✔
229

230
                // since treemap uses different data types, it needs to be transformed
231
                state.hasFunnel && $$.updateFunnel($$.data.targets);
63✔
232

233
                // since treemap uses different data types, it needs to be transformed
234
                state.hasTreemap && $$.updateTargetsForTreemap($$.data.targets);
63✔
235

236
                // Update current state chart type and elements list after redraw
237
                $$.updateTypesElements();
63✔
238

239
                const targets = $el.svg.selectAll(targetIds.map(id => $$.selectorTarget(id)));
87✔
240

241
                $T(targets)
63✔
242
                        .style("opacity", "0")
243
                        .remove()
244
                        .call(endall, done);
245
        }
246
};
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