title: Smooth transition in D3.js realtime chart based on stream of data
body: <p>I want to create simple realtime line chart based on a stream of data with smooth transition after each new datapoint arrives. </p>
  
  <p>I use latest d3 (5.5.0) and rxjs (6.2.1). My mock data stream is 1s interval outputting 12-elements array each second. This array contains 1 new element and 11 from previous output. Subscribe invokes update functions which redraws the chart. 
  I want to use enter and exit functions from d3 -  I'feel like its the right way to handle this. </p>
  
  <p>My result has a flaw - each new point triggers transition with duration od 1s, but sometimes, new point arrives 999ms so this transition never actually fires. Duration around 950ms works almost always but there is noticeable pause betweend transitions. Official and less official examples are for older versions (no enter/exit) or not data invoked - the update functions calls itself recursively and withing self updates values, which is not what I desire.</p>
  
  <p>How can I ensure smooth transition, without staggering effect while stream invokes chart redrawing function ?</p>
  
  <p>UPDATE: As per @Andrew Reid's comment I see enter/exit selections are not for my case here. I updated the code and the problem still occurs. </p>
  
  <p>The code:</p>
  
  <pre><code>import { axisBottom, axisLeft, easeLinear, line, scaleLinear, ScaleLinear, select, selectAll } from 'd3';
  import * as React from 'react';
  import { Observable } from 'rxjs/internal/Observable';
  import { interval } from 'rxjs/internal/observable/interval';
  import { flatMap, map, scan } from 'rxjs/operators';
  import './App.css';
  
  class App extends React.Component {
    private vis: any;
    private xScale: ScaleLinear&lt;number, number&gt;;
    private yScale: ScaleLinear&lt;number, number&gt;;
    private margin = {top: 20, right: 20, bottom: 20, left: 20};
    private width = 1000 - this.margin.left - this.margin.right;
    private height = 500 - this.margin.top - this.margin.bottom;
    private xRange: ReadonlyArray&lt;any&gt; = [0, this.width];
    private yRange: ReadonlyArray&lt;any&gt; = [this.height, 0];
    private line: any;
    private streamsCount = 1;
    private dataStream$: Observable&lt;any&gt;;
  
    public render() {
      return (
          &lt;div className='App'&gt;
            &lt;svg id='visualisation' width='1000' height='500'/&gt;
  
          &lt;/div&gt;
      );
    }
  
    public componentDidMount() {
      this.vis = select('#visualisation')
          .attr('width', this.width + this.margin.left + this.margin.right)
          .attr('height', this.height + this.margin.top + this.margin.bottom)
          .append('g')
          .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
      this.dataStream$ = this.genData();
      this.dataStream$.subscribe(c =&gt; {
        console.log(c);
        if (c.length &gt;= 10) {
          this.update(c);
        }
      }, er =&gt; console.log(er));
      this.setupChart();
    }
  
    private genData(): Observable&lt;any&gt; {
      return interval(1000)
        .pipe(
          flatMap(el =&gt; {
            const out: any[] = [];
            for (let i = 0; i &lt; this.streamsCount; i++) {
              out.push({
                x: el,
                y: (Math.floor(Math.random() * 10)) - 5
              });
            }
            return out;
          }),
          scan((acc: any[], x) =&gt; {
            if (acc.length &lt; 12) {
              for (let i = acc.length; i &lt;= 12; i++) {
                acc.push({
                  x: i,
                  y: (Math.floor(Math.random() * 10)) - 5
                });
              }
            } else if (acc.length &gt;= 12) {
              acc.shift();
            }
            return [...acc, x];
          }, []),
          map(el =&gt; el.map((e: any) =&gt; e.y))
        );
    }
  
    private update(data: any) {
      // this.data = [...this.data, (Math.random() * 10 | 0) - 5];
  
      selectAll('#line')
          .datum(data)
          .attr('d', this.line)
          .attr('transform', null)
          .transition()
          .duration(1000)
          .ease(easeLinear)
          .attr('transform', `translate(${this.xScale(-1)},0)`);
  
      // this.data.shift();
    }
  
    private setupChart() {
      this.setupScales();
      this.setupAxes();
  
      this.line = line()
          .x((d: any, i: number) =&gt; this.xScale(i))
          .y((d: any) =&gt; this.yScale(d));
  
      this.vis.append('path')
          .datum([])
          .attr('id', 'line')
          .attr('class', 'line')
          .attr('d', this.line)
          .transition()
          .duration(50)
          .ease(easeLinear);
    }
  
    private setupScales() {
      this.xScale = scaleLinear()
          .domain([0, 10])
          .range(this.xRange);
  
      this.yScale = scaleLinear()
          .domain([-5, 5])
          .range(this.yRange);
    }
  
    private setupAxes() {
      const axes = this.vis.append('g')
          .attr('class', 'axes');
      axes
          .append('g')
          .attr('transform', `translate(0,${this.yScale(-5)})`)
          .call(axisBottom(this.xScale));
  
      axes
          .append('g')
          .attr('transform', `translate(${this.xScale(0)},0)`)
          .call(axisLeft(this.yScale));
    }
  }
  
  export default App;
  </code></pre>