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

keplergl / kepler.gl / 24978436829

27 Apr 2026 05:38AM UTC coverage: 59.429% (+0.01%) from 59.418%
24978436829

push

github

web-flow
fix: time range filter histogram bar alignment and animation window padding (#3385)

* wip

* add guards to only time range

* address comments

* fix test

6843 of 13805 branches covered (49.57%)

Branch coverage included in aggregate %.

55 of 70 new or added lines in 4 files covered. (78.57%)

3 existing lines in 2 files now uncovered.

14102 of 21439 relevant lines covered (65.78%)

75.84 hits per line

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

15.85
/src/components/src/common/animation-control/animation-controller.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {Component} from 'react';
5
import {bisectLeft} from 'd3-array';
6
import {requestAnimationFrame, cancelAnimationFrame} from 'global/window';
7
import Console from 'global/console';
8
import {BASE_SPEED, FPS, ANIMATION_WINDOW} from '@kepler.gl/constants';
9
import {Timeline} from '@kepler.gl/types';
10

11
interface AnimationControllerProps<T extends number | number[]> {
12
  isAnimating?: boolean;
13
  speed?: number;
14
  updateAnimation?: (x: T) => void;
15
  setTimelineValue: (x: T) => void;
16
  timeline?: Timeline;
17
  animationWindow?: string;
18
  steps?: number[] | null;
19
  domain: number[] | null;
20
  value: T;
21
  baseSpeed?: number;
22
  children?: (
23
    isAnimating: boolean | undefined,
24
    startAnimation: () => void,
25
    pauseAnimation: () => void,
26
    resetAnimation: () => void,
27
    timeline: Timeline | undefined,
28
    setTimelineValue: (x: T) => void
29
  ) => React.ReactElement | null;
30
}
31

32
class AnimationControllerType<T extends number | number[]> extends Component<
33
  AnimationControllerProps<T>
34
> {}
35

36
function AnimationControllerFactory(): typeof AnimationControllerType {
37
  /**
38
   * 4 Animation Window Types
39
   * 1. free
40
   *  |->  |->
41
   * Current time is a fixed range, animate a moving window that calls next animation frames continuously
42
   * The increment id based on domain / BASE_SPEED * SPEED
43
   *
44
   * 2. incremental
45
   * |    |->
46
   * Same as free, current time is a growing range, only the max value of range increment during animation.
47
   * The increment is also based on domain / BASE_SPEED * SPEED
48
   *
49
   * 3. point
50
   * o -> o
51
   * Current time is a point, animate a moving point calls next animation frame continuously
52
   * The increment is based on domain / BASE_SPEED * SPEED
53
   *
54
   * 4. interval
55
   * o ~> o
56
   * Current time is a point. An array of sorted time steps are provided,
57
   * animate a moving point jumps to the next step
58
   */
59
  class AnimationController<T extends number | number[]> extends Component<
60
    AnimationControllerProps<T>
61
  > {
62
    static defaultProps = {
14✔
63
      baseSpeed: BASE_SPEED,
64
      speed: 1,
65
      steps: null,
66
      animationWindow: ANIMATION_WINDOW.free
67
    };
68

69
    state = {
16✔
70
      isAnimating: false
71
    };
72

73
    componentDidMount() {
74
      this._startOrPauseAnimation();
16✔
75
    }
76

77
    componentDidUpdate() {
78
      this._startOrPauseAnimation();
×
79
    }
80

81
    componentWillUnmount() {
82
      if (this._timer) {
×
83
        cancelAnimationFrame(this._timer);
×
84
      }
85
    }
86

87
    _timer = null;
16✔
88
    _startTime = 0;
16✔
89

90
    _startOrPauseAnimation() {
91
      const {isAnimating, speed = 1} = this.props;
16!
92
      if (!this._timer && isAnimating && speed > 0) {
16!
93
        this._startAnimation();
×
94
      } else if (this._timer && !isAnimating) {
16!
95
        this._pauseAnimation();
×
96
      }
97
    }
98

99
    _animate = delay => {
16✔
100
      this._startTime = new Date().getTime();
×
101

102
      const loop = () => {
×
103
        const current = new Date().getTime();
×
104
        const delta = current - this._startTime;
×
105

106
        if (delta >= delay) {
×
107
          this._nextFrame();
×
108
          this._startTime = new Date().getTime();
×
109
        } else {
110
          this._timer = requestAnimationFrame(loop);
×
111
        }
112
      };
113

114
      this._timer = requestAnimationFrame(loop);
×
115
    };
116

117
    _resetAnimationByDomain = () => {
16✔
118
      const {domain, value, animationWindow, updateAnimation} = this.props;
×
119
      if (!domain) {
×
120
        return;
×
121
      }
UNCOV
122
      const setTimelineValue = updateAnimation || this.props.setTimelineValue;
×
123

124
      if (Array.isArray(value)) {
×
NEW
125
        const windowSize = value[1] - value[0];
×
126
        if (animationWindow === ANIMATION_WINDOW.incremental) {
×
127
          setTimelineValue([value[0], value[0] + 1] as T);
×
128
        } else {
NEW
129
          const startValue = domain[0] - windowSize;
×
NEW
130
          setTimelineValue([startValue, startValue + windowSize] as T);
×
131
        }
132
      } else {
133
        setTimelineValue(domain[0] as T);
×
134
      }
135
    };
136

137
    _resetAnimationByTimeStep = () => {
16✔
138
      const {steps = null, updateAnimation} = this.props;
×
139
      if (!steps) return;
×
140
      // interim solution while we fully migrate filter and layer controllers
141
      const setTimelineValue = updateAnimation || this.props.setTimelineValue;
×
142

143
      // go to the first steps
144
      setTimelineValue([steps[0], 0] as T);
×
145
    };
146

147
    _resetAnimation = () => {
16✔
148
      if (this.props.animationWindow === ANIMATION_WINDOW.interval) {
×
149
        this._resetAnimationByTimeStep();
×
150
      } else {
151
        this._resetAnimationByDomain();
×
152
      }
153
    };
154

155
    _startAnimation = () => {
16✔
156
      const {speed = 1} = this.props;
×
157
      this._clearTimer();
×
158
      if (speed > 0) {
×
159
        if (this.props.animationWindow === ANIMATION_WINDOW.interval) {
×
160
          // animate by interval
161
          // 30*600
162
          const {steps} = this.props;
×
163
          if (!Array.isArray(steps) || !steps.length) {
×
164
            Console.warn('animation steps should be an array');
×
165
            return;
×
166
          }
167
          // when speed = 1, animation should loop through 600 frames at 60 FPS
168
          // calculate delay based on # steps
169
          const delay = (BASE_SPEED * (1000 / FPS)) / steps.length / (speed || 1);
×
170
          this._animate(delay);
×
171
        } else {
172
          this._timer = requestAnimationFrame(this._nextFrame);
×
173
        }
174
      }
175
      this.setState({isAnimating: true});
×
176
    };
177

178
    _clearTimer = () => {
16✔
179
      if (this._timer) {
×
180
        cancelAnimationFrame(this._timer);
×
181
        this._timer = null;
×
182
      }
183
    };
184

185
    _pauseAnimation = () => {
16✔
186
      this._clearTimer();
×
187
      this.setState({isAnimating: false});
×
188
    };
189

190
    _nextFrame = () => {
16✔
191
      this._timer = null;
×
192
      const nextValue =
193
        this.props.animationWindow === ANIMATION_WINDOW.interval
×
194
          ? this._nextFrameByTimeStep()
195
          : this._nextFrameByDomain();
196

197
      // interim solution while we fully migrate filter and layer controllers
198
      const setTimelineValue = this.props.updateAnimation || this.props.setTimelineValue;
×
199
      setTimelineValue(nextValue as T);
×
200
    };
201

202
    _nextFrameByDomain() {
203
      const {domain, value, speed = 1, baseSpeed = 600, animationWindow} = this.props;
×
204
      if (!domain) {
×
205
        return;
×
206
      }
207
      const delta = ((domain[1] - domain[0]) / baseSpeed) * speed;
×
208

209
      // loop when reaches the end
210
      // current time is a range
211
      if (Array.isArray(value)) {
×
212
        let value0: number;
213
        let value1: number;
NEW
214
        const windowSize = value[1] - value[0];
×
215
        if (animationWindow === ANIMATION_WINDOW.incremental) {
×
216
          const lastFrame = value[1] + delta > domain[1];
×
217
          value0 = value[0];
×
218
          value1 = lastFrame ? value[0] + 1 : value[1] + delta;
×
219
        } else {
UNCOV
220
          const lastFrame = value[0] + delta > domain[1];
×
NEW
221
          const startValue = domain[0] - windowSize;
×
NEW
222
          value0 = lastFrame ? startValue : value[0] + delta;
×
NEW
223
          value1 = value0 + windowSize;
×
224
        }
225
        return [value0, value1];
×
226
      }
227

228
      // current time is a point
229
      return Number(value) + delta > domain[1] ? domain[0] : Number(value) + delta;
×
230
    }
231

232
    _nextFrameByTimeStep() {
233
      const {steps = null, value} = this.props;
×
234
      if (!steps) return;
×
235
      const val = Array.isArray(value) ? value[0] : Number(value);
×
236
      const index = bisectLeft(steps, val);
×
237
      const nextIdx = index >= steps.length - 1 ? 0 : index + 1;
×
238

239
      // why do we need to pass an array of two objects? are we reading nextIdx at some point?
240
      // _nextFrameByDomain only returns one value
241
      return [steps[nextIdx], nextIdx];
×
242
    }
243

244
    render() {
245
      const {isAnimating} = this.state;
16✔
246
      const {children} = this.props;
16✔
247

248
      return typeof children === 'function'
16!
249
        ? children(
250
            isAnimating,
251
            this._startAnimation,
252
            this._pauseAnimation,
253
            this._resetAnimation,
254
            this.props.timeline,
255
            this.props.setTimelineValue
256
          )
257
        : null;
258
    }
259
  }
260

261
  return AnimationController;
14✔
262
}
263

264
export default AnimationControllerFactory;
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