断断续续的过渡:同时翻译多个路径(和 xaxis)

处理器

目标是创建具有多个轨迹的平滑滚动实时图。

我能够为单个跟踪执行此操作,但是当我添加更多行进行过渡时,动画似乎变得一团糟。我有一种感觉,过渡正在循环和碰撞,但我不知道如何防止这种情况发生。

如果您N_CH = 1在代码段中进行设置,则事情运行顺利。当它设置为N_CH = 4然后动画变得生涩(似乎过渡没有完全完成)并且(有趣的是)x 轴滚动似乎变得比 时快 4 倍N_CH = 1

您可以通过更改tick()函数中的变换以匹配通道数(即iScale(-4)for N_CH = 4来恢复平滑度,但这不是“正确的”,因为翻译速度人为地快。最后,我需要实时准确的时间测量。

我尝试了各种不同的方法,包括:

  • 向组添加痕迹并尝试翻译该组
  • 重构data对象并允许d3通过selectAll()调用遍历数据结构

……结果似乎总是一样。

// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']

// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
  channelData = [];
  for (let i = 0; i < N_PTS; i++) {
    channelData.push({
      x: Date.now() + i * 1000,
      y: ch + Math.random()
    })
  }
  data.push({
    name: "CH" + ch,
    values: channelData
  });
}

// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", margin.left)
  .attr("y", margin.bottom)
  .attr("width", width - margin.left - margin.right)
  .attr("height", height - margin.top - margin.bottom);

// set index scale for data buffer position/transition
var iScale = d3.scaleLinear()
  .range([0, width - margin.right])
  .domain([0, data[0].values.length - 1]);

// set up x-axis scale for data x units (time)
var xScale = d3.scaleUtc()
  .range([margin.left, width - margin.right])

// add x-axis to svg
var xAxis = svg.append("g")
  .attr("class", "x-axis")
  .attr("transform", `translate(0, ${height - margin.top})`)
  .call(d3.axisBottom(xScale));

// set up y-axis
var yScale = d3.scaleLinear()
  .range([height - margin.top, margin.bottom]);

// add y-axis to svg
var yAxis = svg.append("g")
  .attr("class", "y-axis")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisLeft(yScale));

// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));

// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));

// define the line
var line = d3.line()
  .x((d, i) => iScale(i))
  .y(d => yScale(d.y));

// make a group where we will append our paths
traces = svg.append("g")
  .attr("clip-path", "url(#clip)")

for (let ch=0; ch<N_CH; ch++) {
  traces.append("path")
    .datum(data[ch].values)
    .attr("id", `trace-${ch}`)
    .attr("class", "trace")
    .attr("d", line)
    .attr("stroke", colors[ch])
    .attr("fill", "none")
    .attr("stroke-width", 1.5)
    .attr("transform", "translate(0)")
}
// end initialize ////////////////////

// animate
tick();

function tick() {
  // add data to buffer
  let lastData;
  for (let ch = 0; ch < N_CH; ch++) {
    lastData = data[ch].values[data[ch].values.length - 1];
    data[ch].values.push({
      x: lastData.x + 1000,
      y: ch + Math.random()
    });
  }

  // update individual trace path data
  for (let ch = 0; ch < N_CH; ch++) {
    traces.select(`#trace-${ch}`)
      .attr("d", line)
  }

  // animate transition
  traces
    .selectAll('.trace')
    .attr("transform", "translate(0)")
    .transition().duration(1000).ease(d3.easeLinear)
    .attr("transform", `translate(${iScale(-1)}, 0)`)
    .on("end", tick)
    
  // update the domain
  xScale.domain(d3.extent(data[0].values, d => d.x));
  
  // animate/redraw axis
  xAxis
    .transition().duration(1000).ease(d3.easeLinear)
    .call(d3.axisBottom(xScale));

  for (let ch=0; ch<N_CH; ch++) {
    data[ch].values.shift();
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>

安德鲁·里德

这里有几个问题:

  • xScale 与 iScale:

您根据 iScale 绘制数据,但根据 xScale 绘制轴:这里立即存在差异:每个比例的范围不同。但是没有理由不应该对两者使用相同的比例:这样您就不会在绘图和轴之间有任何差异。如果您删除剪辑路径并删除刻度函数,您会注意到您的线条最初并未呈现在您期望的位置:

在此处输入图片说明

  • 滥用 transition.end()

D3 的转换事件侦听器用于每个转换。您正在转换许多元素,这会在每一行完成时触发。因此,在四行第一次完成转换后,您触发了四次刻度函数:这会导致各种混乱,因为该函数旨在被调用一次以一次转换所有行。

在重新阅读这个问题时,您发现了调用刻度函数 4x 而不是一次的问题:

您可以通过更改 tick() 函数中的变换以匹配通道数(即 N_CH = 4 的 iScale(-4))来恢复平滑度,但这不是“正确的”,因为转换速度人为地快速。

如果我们解决这个问题以便我们调用一次 tick 函数,当所有的线转换完成后,我们就解决了平滑问题:

// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']

// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
  channelData = [];
  for (let i = 0; i < N_PTS; i++) {
    channelData.push({
      x: Date.now() + i * 1000,
      y: ch + Math.random()
    })
  }
  data.push({
    name: "CH" + ch,
    values: channelData
  });
}

// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", margin.left)
  .attr("y", margin.bottom)
  .attr("width", width - margin.left - margin.right)
  .attr("height", height - margin.top - margin.bottom);

// set index scale for data buffer position/transition
var iScale = d3.scaleLinear()
  .range([0, width - margin.right])
  .domain([0, data[0].values.length - 1]);

// set up x-axis scale for data x units (time)
var xScale = d3.scaleUtc()
  .range([margin.left, width - margin.right])

// add x-axis to svg
var xAxis = svg.append("g")
  .attr("class", "x-axis")
  .attr("transform", `translate(0, ${height - margin.top})`)
  .call(d3.axisBottom(xScale));

// set up y-axis
var yScale = d3.scaleLinear()
  .range([height - margin.top, margin.bottom]);

// add y-axis to svg
var yAxis = svg.append("g")
  .attr("class", "y-axis")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisLeft(yScale));

// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));

// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));

// define the line
var line = d3.line()
  .x((d, i) => iScale(i))
  .y(d => yScale(d.y));

// make a group where we will append our paths
traces = svg.append("g")
  .attr("clip-path", "url(#clip)")

for (let ch=0; ch<N_CH; ch++) {
  traces.append("path")
    .datum(data[ch].values)
    .attr("id", `trace-${ch}`)
    .attr("class", "trace")
    .attr("d", line)
    .attr("stroke", colors[ch])
    .attr("fill", "none")
    .attr("stroke-width", 1.5)
    .attr("transform", "translate(0)")
}
// end initialize ////////////////////

// animate
tick();

function tick() {
  // add data to buffer
  let lastData;
  for (let ch = 0; ch < N_CH; ch++) {
    lastData = data[ch].values[data[ch].values.length - 1];
    data[ch].values.push({
      x: lastData.x + 1000,
      y: ch + Math.random()
    });
  }

  // update individual trace path data
  for (let ch = 0; ch < N_CH; ch++) {
    traces.select(`#trace-${ch}`)
      .attr("d", line)
  }

  // animate transition
  traces
    .selectAll('.trace')
    .attr("transform", "translate(0)")
    .transition().duration(1000).ease(d3.easeLinear)
    .attr("transform", `translate(${iScale(-1)}, 0)`)
    .end().then(tick);
    
  // update the domain
  xScale.domain(d3.extent(data[0].values, d => d.x));
  
  // animate/redraw axis
  xAxis
    .transition().duration(1000).ease(d3.easeLinear)
    .call(d3.axisBottom(xScale));

  for (let ch=0; ch<N_CH; ch++) {
    data[ch].values.shift();
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="chart"></div>

在上面我使用 transition.end() 在所有选定的元素完成转换时返回一个承诺。我已经升级了您的 D3 版本,因为这是一个较新的功能:

 .end().then(tick);
  • 改进:

您的代码使用循环来附加和修改元素。这会产生额外的开销:在 DOM 中选择元素需要时间,您必须识别每一行以便您可以再次重新选择它,并且您必须在绑定数据方面做一些额外的工作。让我们用 d3 输入/更新周期来简化它:

创建要开始的行:

let lines = traces.selectAll(null)
  .data(data)
  .enter()
  .append("path")
  .attr("d", d=>line(d.values))
  .attr("stroke", (d,i)=>colors[i])
  .attr("fill", "none")
  .attr("stroke-width", 1.5)
  .attr("transform","translate(0,0)");

现在在 update/tick 函数中,我们可以轻松修改绑定数据:

  lines.each(function(d,i) {
     d.values.push({
      x: d.values[d.values.length-1].x + dt,
      y: i + Math.random()
    })
  })
  .attr("d", d=>line(d.values))

我们可以删除每行的第一个数据点:

 lines.each(d=>d.values.shift());

一般来说,(显式)循环在使用 D3 操作 SVG 元素时非常罕见,因为它与 D3 的设计原则背道而驰。有关为什么这很重要以及它如何有用的一些讨论,请参见此处

连同移除 iScale 并使用 transition.end(),我们可能会得到类似的结果:

// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']

// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
  channelData = [];
  for (let i = 0; i < N_PTS; i++) {
    channelData.push({
      x: Date.now() + i * 1000,
      y: ch + Math.random()
    })
  }
  data.push({
    name: "CH" + ch,
    values: channelData
  });
}

// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
  .select("#chart")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`)

// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", margin.left)
  .attr("y", margin.bottom)
  .attr("width", width - margin.left - margin.right)
  .attr("height", height - margin.top - margin.bottom);

// set up x-axis scale for data x units (time)
var xScale = d3.scaleTime()
  .range([margin.left, width - margin.right])
  .domain(d3.extent(data[0].values,d=>d.x))

// add x-axis to svg
var xAxis = svg.append("g")
  .attr("class", "x-axis")
  .attr("transform", `translate(0, ${height - margin.top})`)
  .call(d3.axisBottom(xScale));

// set up y-axis
var yScale = d3.scaleLinear()
  .range([height - margin.top, margin.bottom]);

// add y-axis to svg
var yAxis = svg.append("g")
  .attr("class", "y-axis")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisLeft(yScale));

// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));

// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));

// define the line
var line = d3.line()
  .x(d => xScale(d.x))
  .y(d => yScale(d.y));

// make a group where we will append our paths
traces = svg.append("g")
  .attr("clip-path", "url(#clip)")

// Create lines:
let lines = traces.selectAll(null)
  .data(data)
  .enter()
  .append("path")
  .attr("d", d=>line(d.values))
  .attr("stroke", (d,i)=>colors[i])
  .attr("fill", "none")
  .attr("stroke-width", 1.5)
  .attr("transform","translate(0,0)");

transition();

function transition() {
  let dt = 1000; // difference in time.
  let dx = xScale(d3.timeMillisecond.offset(xScale.domain()[0],dt)) - xScale.range()[0]; // difference in pixels.

  lines.each(function(d,i) {
     d.values.push({
      x: d.values[d.values.length-1].x + dt,
      y: i + Math.random()
    })
  })
  .attr("d", d=>line(d.values))
  .transition()
  .duration(1000)
  .attr("transform",`translate(${-dx}, 0)`)  
  .ease(d3.easeLinear)
  .end().then(function() {
      lines.each(d=>d.values.shift())
       .attr("transform","translate(0,0)")
      transition();
  })
  
xScale.domain(xScale
         .domain()
         .map(d=>d3.timeMillisecond.offset(d,dt)))
  xAxis
    .transition().duration(1000).ease(d3.easeLinear)
    .call(d3.axisBottom(xScale))
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div id="chart"></div>

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章