基于数据流的D3.js实时图表中的平滑过渡

时间:2018-07-08 15:33:44

标签: javascript d3.js

我想基于在每个新数据点到达后具有平稳过渡的数据流创建简单的实时折线图。

我使用最新的d3(5.5.0)和rxjs(6.2.1)。我的模拟数据流是1s间隔,每秒输出12个元素数组。该数组包含1个新元素和11个来自先前输出的元素。订阅会调用更新功能,从而重绘图表。 我想使用d3中的enter和exit函数-我觉得它是处理此问题的正确方法。

我的结果有一个缺陷-每个新点都会以持续时间od 1s触发转换,但是有时新点到达999ms,因此这种转换实际上不会触发。大约950毫秒的持续时间几乎始终有效,但过渡之间会出现明显的暂停。官方和较不官方的示例适用于较旧的版本(无输入/退出)或未调用数据-更新功能以递归方式调用自身并具有自我更新值,这不是我想要的。

在流调用图表重绘功能时,如何确保平滑过渡而又不产生交错效应?

更新:根据@Andrew Reid的评论,我看到进入/退出选择不适用于我的情况。我更新了代码,问题仍然出现。

代码:

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<number, number>;
  private yScale: ScaleLinear<number, number>;
  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<any> = [0, this.width];
  private yRange: ReadonlyArray<any> = [this.height, 0];
  private line: any;
  private streamsCount = 1;
  private dataStream$: Observable<any>;

  public render() {
    return (
        <div className='App'>
          <svg id='visualisation' width='1000' height='500'/>

        </div>
    );
  }

  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 => {
      console.log(c);
      if (c.length >= 10) {
        this.update(c);
      }
    }, er => console.log(er));
    this.setupChart();
  }

  private genData(): Observable<any> {
    return interval(1000)
      .pipe(
        flatMap(el => {
          const out: any[] = [];
          for (let i = 0; i < this.streamsCount; i++) {
            out.push({
              x: el,
              y: (Math.floor(Math.random() * 10)) - 5
            });
          }
          return out;
        }),
        scan((acc: any[], x) => {
          if (acc.length < 12) {
            for (let i = acc.length; i <= 12; i++) {
              acc.push({
                x: i,
                y: (Math.floor(Math.random() * 10)) - 5
              });
            }
          } else if (acc.length >= 12) {
            acc.shift();
          }
          return [...acc, x];
        }, []),
        map(el => el.map((e: any) => 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) => this.xScale(i))
        .y((d: any) => 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;

1 个答案:

答案 0 :(得分:0)

在我收集了更多的知识之后,我设法解决了这个问题。

在D3中,计划在相同选择单元格上一个单元的下一个转换。因此,为了安排上一个过渡之后的过渡,我捕获了活动过渡并将on('start')设置为安排下一个过渡。我认为“结束”在这里应该更好,但没有用。我读过某处的内容是因为取消的转换不会调用结束事件。但是我不确定这里是否是这种情况。下面是固定代码(片段):

private update(data: any) {
    const path = select('#line');
    const pathTransition = path.transition('line');
    if (pathTransition) {
      pathTransition.on('start', () => {
        path.datum(data)
            .attr('transform', null)
            .attr('d', this.line)
            .transition('line')
            .duration(this.SAMPLING)
            .ease(easeLinear)
            .attr('transform', `translate(${this.xScale(-1)},0)`);
      });
    } else {
      path.datum(data)
          .attr('transform', null)
          .attr('d', this.line)
          .transition('line')
          .duration(this.SAMPLING)
          .ease(easeLinear)
          .attr('transform', `translate(${this.xScale(-1)},0)`);
    }
  }