首页 > 解决方案 > 基于数据流的 D3.js 实时图表中的平滑过渡

问题描述

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

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

我的结果有一个缺陷——每个新点都会触发持续时间为 1 秒的转换,但有时,新点会在 999 毫秒内到达,因此这个转换实际上不会触发。大约 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;

标签: javascriptd3.js

解决方案


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

在 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)`);
    }
  }

推荐阅读