• 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

91.82
/src/Chart/api/flow.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 */
5
import {isDefined, isTabVisible, isValue, parseDate} from "../../module/util";
6

7
export default {
8
        /**
9
         * Flow data to the chart.<br><br>
10
         * By this API, you can append new data points to the chart.
11
         * @function flow
12
         * @instance
13
         * @memberof Chart
14
         * @param {object} args The object can consist with following members:<br>
15
         *
16
         *    | Key | Type | Description |
17
         *    | --- | --- | --- |
18
         *    | json | Object | Data as JSON format (@see [data․json](Options.html#.data%25E2%2580%25A4json)) |
19
         *    | rows | Array | Data in array as row format (@see [data․rows](Options.html#.data%25E2%2580%25A4json)) |
20
         *    | columns | Array | Data in array as column format (@see [data․columns](Options.html#.data%25E2%2580%25A4columns)) |
21
         *    | to | String | The lower x edge will move to that point. If not given, the lower x edge will move by the number of given data points |
22
         *    | length | Number | The lower x edge will move by the number of this argument |
23
         *    | duration | Number | The duration of the transition will be specified value. If not given, transition.duration will be used as default |
24
         *    | done | Function | The specified function will be called when flow ends |
25
         *
26
         * - **NOTE:**
27
         *   - If json, rows and columns given, the data will be loaded.
28
         *   - If data that has the same target id is given, the chart will be appended.
29
         *   - Otherwise, new target will be added. One of these is required when calling.
30
         *   - If json specified, keys is required as well as data.json.
31
         *          - If tab isn't visible(by evaluating `document.hidden`), will not be executed to prevent unnecessary work.
32
         * @example
33
         * // 2 data points will be appended to the tail and popped from the head.
34
         * // After that, 4 data points will be appended and no data points will be poppoed.
35
         * chart.flow({
36
         *  columns: [
37
         *    ["x", "2018-01-11", "2018-01-21"],
38
         *    ["data1", 500, 200],
39
         *    ["data2", 100, 300],
40
         *    ["data3", 200, 120]
41
         *  ],
42
         *  to: "2013-01-11",
43
         *  done: function () {
44
         *    chart.flow({
45
         *      columns: [
46
         *        ["x", "2018-02-11", "2018-02-12", "2018-02-13", "2018-02-14"],
47
         *        ["data1", 200, 300, 100, 250],
48
         *        ["data2", 100, 90, 40, 120],
49
         *        ["data3", 100, 100, 300, 500]
50
         *      ],
51
         *      length: 2,
52
         *      duration: 1500
53
         *    });
54
         *  }
55
         * });
56
         */
57
        flow(args): void {
58
                const $$ = this.internal;
72✔
59
                let data;
60

61
                if (args.json || args.rows || args.columns) {
72!
62
                        $$.convertData(args, res => {
72✔
63
                                data = res;
72✔
64
                                _();
72✔
65
                        });
66
                }
67

68
                /**
69
                 * Process flows
70
                 * @private
71
                 */
72
                function _(): void {
73
                        let domain;
74
                        let length: number = 0;
72✔
75
                        let tail = 0;
72✔
76
                        let diff;
77
                        let to;
78

79
                        if ($$.state.redrawing || !data || !isTabVisible()) {
72✔
80
                                return;
3✔
81
                        }
82

83
                        $$.flushCanvasFlow?.();
69✔
84

85
                        const notfoundIds: string[] = [];
69✔
86
                        const orgDataCount = $$.getMaxDataCount();
69✔
87
                        const targets = $$.convertDataToTargets(data, true);
69✔
88
                        const isTimeSeries = $$.axis.isTimeSeries();
69✔
89

90
                        // Update/Add data
91
                        $$.data.targets.forEach(t => {
69✔
92
                                let found = false;
87✔
93

94
                                for (let i = 0; i < targets.length; i++) {
87✔
95
                                        if (t.id === targets[i].id) {
75!
96
                                                found = true;
75✔
97

98
                                                if (t.values[t.values.length - 1]) {
75!
99
                                                        tail = t.values[t.values.length - 1].index + 1;
75✔
100
                                                }
101

102
                                                const values = targets[i].values;
75✔
103

104
                                                length = values.length;
75✔
105

106
                                                for (let j = 0; j < length; j++) {
75✔
107
                                                        values[j].index = tail + j;
129✔
108

109
                                                        if (!isTimeSeries) {
129✔
110
                                                                values[j].x = tail + j;
75✔
111
                                                        }
112

113
                                                        t.values.push(values[j]);
129✔
114
                                                }
115

116
                                                targets.splice(i, 1);
75✔
117
                                                break;
75✔
118
                                        }
119
                                }
120

121
                                !found && notfoundIds.push(t.id);
87✔
122
                        });
123

124
                        // Append null for not found targets
125
                        $$.data.targets.forEach(t => {
69✔
126
                                for (let i = 0; i < notfoundIds.length; i++) {
87✔
127
                                        if (t.id === notfoundIds[i]) {
30✔
128
                                                // target can have no values when fully flowed out
129
                                                if (!t.values[t.values.length - 1]) {
12!
NEW
130
                                                        continue;
×
131
                                                }
132

133
                                                tail = t.values[t.values.length - 1].index + 1;
12✔
134

135
                                                for (let j = 0; j < length; j++) {
12✔
136
                                                        t.values.push({
21✔
137
                                                                id: t.id,
138
                                                                index: tail + j,
139
                                                                x: isTimeSeries ? $$.getOtherTargetX(tail + j) : tail + j,
21✔
140
                                                                value: null
141
                                                        });
142
                                                }
143
                                        }
144
                                }
145
                        });
146

147
                        // Generate null values for new target
148
                        if ($$.data.targets.length) {
69✔
149
                                targets.forEach(t => {
60✔
150
                                        const firstIndex = $$.data.targets[0].values[0].index;
6✔
151
                                        const values = t.values;
6✔
152
                                        const valueLength = values.length;
6✔
153
                                        const missingLength = Math.max(tail - firstIndex, 0);
6✔
154

155
                                        if (missingLength) {
6!
156
                                                values.length = valueLength + missingLength;
6✔
157

158
                                                for (let i = valueLength - 1; i >= 0; i--) {
6✔
159
                                                        values[i + missingLength] = values[i];
12✔
160
                                                }
161

162
                                                for (let i = 0; i < missingLength; i++) {
6✔
163
                                                        const index = firstIndex + i;
18✔
164

165
                                                        values[i] = {
18✔
166
                                                                id: t.id,
167
                                                                index,
168
                                                                x: isTimeSeries ? $$.getOtherTargetX(index) : index,
18✔
169
                                                                value: null
170
                                                        };
171
                                                }
172
                                        }
173

174
                                        for (let i = missingLength; i < values.length; i++) {
6✔
175
                                                const v = values[i];
12✔
176

177
                                                v.index += tail;
12✔
178

179
                                                if (!isTimeSeries) {
12✔
180
                                                        v.x += tail;
6✔
181
                                                }
182
                                        }
183
                                });
184
                        }
185

186
                        for (let i = 0; i < targets.length; i++) {
69✔
187
                                $$.data.targets.push(targets[i]); // add remained
15✔
188
                        }
189

190
                        // check data count because behavior needs to change when it"s only one
191
                        // const dataCount = $$.getMaxDataCount();
192
                        const baseTarget = $$.data.targets[0];
69✔
193
                        const baseValue = baseTarget.values[0];
69✔
194

195
                        // Update length to flow if needed
196
                        if (isDefined(args.to)) {
69✔
197
                                length = 0;
3✔
198
                                to = isTimeSeries ? parseDate.call($$, args.to) : args.to;
3!
199

200
                                baseTarget.values.forEach(v => {
3✔
201
                                        v.x < to && length++;
15!
202
                                });
203
                        } else if (isDefined(args.length)) {
66✔
204
                                length = args.length;
6✔
205
                        }
206

207
                        // If only one data, update the domain to flow from left edge of the chart
208
                        if (!orgDataCount) {
69✔
209
                                if (isTimeSeries) {
9✔
210
                                        diff = baseTarget.values.length > 1 ?
6✔
211
                                                baseTarget.values[baseTarget.values.length - 1].x - baseValue.x :
212
                                                baseValue.x - $$.getXDomain($$.data.targets)[0];
213
                                } else {
214
                                        diff = 1;
3✔
215
                                }
216

217
                                domain = [baseValue.x - diff, baseValue.x];
9✔
218
                        } else if (orgDataCount === 1 && isTimeSeries) {
60!
219
                                diff = (baseTarget.values[baseTarget.values.length - 1].x - baseValue.x) / 2;
×
220
                                domain = [new Date(+baseValue.x - diff), new Date(+baseValue.x + diff)];
×
221
                        }
222

223
                        domain && $$.updateXDomain(null, true, true, false, domain);
69✔
224

225
                        if ($$.state.isCanvasMode) {
69✔
226
                                const duration = isValue(args.duration) ?
27✔
227
                                        args.duration :
228
                                        $$.config.transition_duration;
229
                                const animated = $$.animateCanvasFlow?.({
27✔
230
                                        done: args.done,
231
                                        duration,
232
                                        length,
233
                                        orgDataCount
234
                                });
235

236
                                if (animated) {
27✔
237
                                        return;
15✔
238
                                }
239

240
                                // Canvas mode has no retained SVG nodes to transition. Mutate the data to the
241
                                // post-flow state, then redraw the final frame.
242
                                if (length && orgDataCount) {
12!
243
                                        $$.data.targets.forEach(d => {
12✔
244
                                                d.values.splice(0, length);
15✔
245
                                        });
246
                                }
247

248
                                $$.state.dirty.data = true;
12✔
249
                                $$.state._eventRectFingerprint = null;
12✔
250

251
                                $$.redraw({
12✔
252
                                        withLegend: true,
253
                                        withTransition: false,
254
                                        withTrimXDomain: false,
255
                                        withUpdateOrgXDomain: true,
256
                                        withUpdateXAxis: true,
257
                                        withUpdateXDomain: true
258
                                });
259
                                $$.updateTypesElements();
12✔
260
                                args.done?.call($$.api);
12✔
261
                                return;
12✔
262
                        }
263

264
                        // Set targets
265
                        $$.updateTargets($$.data.targets);
42✔
266
                        $$.state.dirty.data = true;
42✔
267
                        $$.state._eventRectFingerprint = null;
42✔
268

269
                        // Redraw with new targets
270
                        $$.redraw({
42✔
271
                                flow: {
272
                                        index: baseValue.index,
273
                                        length: length,
274
                                        duration: isValue(args.duration) ?
42✔
275
                                                args.duration :
276
                                                $$.config.transition_duration,
277
                                        done: args.done,
278
                                        orgDataCount: orgDataCount
279
                                },
280
                                withLegend: true,
281
                                withTransition: orgDataCount > 1,
282
                                withTrimXDomain: false,
283
                                withUpdateXAxis: true
284
                        });
285
                }
286
        }
287
};
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