【问题标题】:Do d3 elements need to be redefined with repeated code?d3 元素是否需要用重复的代码重新定义?
【发布时间】:2019-03-16 00:32:19
【问题描述】:

我对 D3 还很陌生,但是我看到的所有示例都重新定义了元素更新时的创建。如果您想更改元素的定义方式(例如将圆形更改为矩形),我可以看到一个论点,但在大多数情况下,我需要定义是相同的。

此示例是this 答案和this 答案的合并。它更接近我的实际用例,但也突出了重复的数量。

希望我在定义这一点的方式上偏离了基础,并且有一种更整洁的方式来做到这一点。或者,我猜答案是“是的,这是这样做的理想方式”。

var svg = d3.select("svg");
d3.select("button").on("click", update);
let color = d3.scaleOrdinal().range(d3.schemeAccent);
let data;
update();

function update() {
  updateData();
  updateNodes();
}

function updateData() {
  let numNodes = ~~(Math.random() * 4 + 10);
  data = d3.range(numNodes).map(function(d) {
    return {
      size: ~~(Math.random() * 20 + 3),
      x: ~~(Math.random() * 600),
      y: ~~(Math.random() * 200)
    };
  });
}

function updateNodes() {
  var node = svg.selectAll(".node").data(data);
  node.exit().remove();
  node
    .enter()
    .append("g")
    .classed("node", true)
    .append("circle")
    .classed("outer", true)
    .attr("fill", d => color(d.size))
    .attr("opacity", 0.5)
    .attr("r", d => d.size * 2)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y)
    .select(function() { return this.parentNode; }) //needs an old style function for this reason:  https://stackoverflow.com/questions/28371982/what-does-this-refer-to-in-arrow-functions-in-es6 .select(()=> this.parentNode) won't work
    .append("circle")
    .classed("inner", true)
    .attr("fill", d => color(d.size))
    .attr("r", d => d.size)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y)
    .select(function() { return this.parentNode; })
    .append("text")
    .attr("x", d => d.x)
    .attr("y", d => d.y)
    .attr("text-anchor", "middle")
    .text(d => d.size);

  node
    .select("circle.inner")
    .transition()
    .duration(1000)
    .attr("fill", d => color(d.size))
    .attr("r", d => d.size)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

  node
    .select("circle.outer")
    .transition()
    .duration(1000)
    .attr("fill", d => color(d.size))
    .attr("opacity", 0.5)
    .attr("r", d => d.size * 2)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

  node
    .select("text")
    .transition()
    .duration(1000)
    .attr("x", d => d.x)
    .attr("y", d => d.y)
    .attr("text-anchor", "middle")
    .text(d => d.size);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<button>Update</button>
<br>
<svg width="600" height="200"></svg>

【问题讨论】:

  • 您无法确定当前代码中的节点是否已更新,因此每次更新都会删除所有现有数据点并用新集合替换它们。

标签: d3.js


【解决方案1】:

您的问题的简单答案是“不,不需要使用重复的代码重新定义元素。”较长的答案(我将尽量保持简短)涉及 d3 进入/更新/退出范式和对象恒常性。

已经有很多关于d3's data binding paradigm的文档;通过考虑绑定到 DOM 元素的数据,我们可以识别 enter 选择,新数据/元素; update 选择,已更改的现有数据/元素;和exit 选择,要删除的数据/元素。在将每个数据加入 DOM 时,使用键函数来唯一标识每个数据,这允许 d3 识别它是新的、更新的还是已从数据集中删除。例如:

var data = [{size: 8, id: 1}, {size: 10, id: 2}, {size: 24, id: 3}];

var nodes = svg.selectAll(".node").data(data, function (d) { return d.id });

// deal with enter / exit / update selections, etc.

// later on
var updated = [{size: 21, id: 1}, {size: 10, id: 4}, {size: 24, id: 3}];

var nodes_now = svg.selectAll(".node")
.data(updated, function (d) { return d.id });

// nodes_now.enter() will contain {size:10, id: 4}
// nodes_now.exit() will contain {size:10, id: 2}

同样,有很多关于这方面的现有信息;请参阅the d3 docsobject constancy 了解更多详情。

如果图表中没有数据/元素正在更新——例如如果可视化只绘制一次并且不需要动画,或者如果每次重绘图表时都替换数据,则无需对update 选择执行任何操作;可以直接在enter 选择上设置适当的属性。在您的示例中,没有关键功能,因此每次更新都会从图表中转储所有旧数据并用新数据重新绘制它。在对 enter 选择执行的转换之后,您实际上不需要任何代码,因为没有可使用的 update 选择。

您可能见过的示例是那些使用更新选择来为图表设置动画的示例。一个典型的模式是

// bind data to elements
var nodes = d3.selectAll('.node')
.data( my_data, d => d.id )

// delete extinct data
nodes.exit().remove()

// add new data items
var nodeEnter = nodes.enter()
.append(el) // whatever the element is
.classed('node', true)
.attr(...) // initialise attributes

// merge the new nodes into the existing selection to create the enter+update selection
// turn the selection into a transition so that any changes will be animated
var nodeUpdate = nodes
.merge(nodesEnter)
.transition()
.duration(1000)

// now set the appropriate values for attributes, etc.
nodeUpdate
.attr(...)

enter+update 选择包含新初始化的节点和已更改值的现有节点,因此任何转换都必须涵盖这两种情况。如果我们想在您的代码中使用这种模式,这可能是一种方法:

  // use the node size as the key function so we have some data persisting between updates
  var node = svg.selectAll(".node").data(data, d => d.size)

  // fade out extinct nodes
  node
    .exit()
    .transition()
    .duration(1000)
    .attr('opacity', 0)
    .remove()

  // save the enter selection as `nodeEnter`
  var nodeEnter = node
    .enter()
    .append("g")
    .classed("node", true)
    .attr("opacity", 0)    // set initial opacity to 0
    // transform the group element, rather than each bit of the group
    .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')')

  nodeEnter
   .append("circle")
    .classed("outer", true)
    .attr("opacity", 0.5)
    .attr("fill", d => color(d.size))
    .attr("r", 0)                     // initialise radius to 0
  .select(function() { return this.parentNode; })
   .append("circle")
    .classed("inner", true)
    .attr("fill", d => color(d.size))
    .attr("r", 0)                     // initialise radius to 0
  .select(function() { return this.parentNode; })
   .append("text")
    .attr("dy", '0.35em')
    .attr("text-anchor", "middle")
    .text(d => d.size)

 // merge enter selection with update selection
 // the following transformations will apply to new nodes and existing nodes
 node = node
    .merge(nodeEnter)
    .transition()
    .duration(1000)

  node
    .attr('opacity', 1)  // fade into view
    .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')')  // move to appropriate location

  node.select("circle.inner")
    .attr("r", d => d.size)      // set radius to appropriate size

  node.select("circle.outer")
    .attr("r", d => d.size * 2)  // set radius to appropriate size

只有正在动画的元素和属性(例如,圆半径或新节点的g 元素的不透明度)或依赖于可能更改的基准面的各个方面(g 变换,它使用 @ 987654336@和d.y的现有节点)需要更新,因此更新代码比输入选择要紧凑得多。

完整演示:

var svg = d3.select("svg");
d3.select("button").on("click", update);
let color = d3.scaleOrdinal().range(d3.schemeAccent);
let data;
update();

function update() {
  updateData();
  updateNodes();
}

function updateData() {
  let numNodes = ~~(Math.random() * 4 + 10);
  data = d3.range(numNodes).map(function(d) {
    return {
      size: ~~(Math.random() * 20 + 3),
      x: ~~(Math.random() * 600),
      y: ~~(Math.random() * 200)
    };
  });
}

function updateNodes() {
  var node = svg.selectAll(".node").data(data, d => d.size)
  node
    .exit()
    .transition()
    .duration(1000)
    .attr('opacity', 0)
    .remove()

  var nodeEnt = node
    .enter()
    .append("g")
    .classed("node", true)
    .attr("opacity", 0)
    .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')')
   
  nodeEnt
    .append("circle")
    .classed("outer", true)
    .attr("opacity", 0)
    .attr("fill", d => color(d.size))
    .attr("r", d => 0)
   .select(function() { return this.parentNode; }) //needs an old style function for this reason:  https://stackoverflow.com/questions/28371982/what-does-this-refer-to-in-arrow-functions-in-es6 .select(()=> this.parentNode) won't work
    .append("circle")
    .classed("inner", true)
    .attr("fill", d => color(d.size))
    .attr("r", 0)
   .select(function() { return this.parentNode; })
    .append("text")
    .attr("dy", '0.35em')
    .attr("text-anchor", "middle")
    .text(d => d.size)
    
 node = node
    .merge(nodeEnt)
    .transition()
    .duration(1000)

  node
    .attr('opacity', 1)
    .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')')
 
  node.select("circle.inner")
    .attr('opacity', 1)
    .attr("r", d => d.size)

  node
    .select("circle.outer")
    .attr("opacity", 0.5)
    .attr("r", d => d.size * 2)

}
<script src="https://d3js.org/d3.v5.min.js"></script>
<button>Update</button>
<br>
<svg width="600" height="200"></svg>

值得注意的是,有很多 d3 示例中包含大量冗余代码。

为了保持简短...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-13
    • 1970-01-01
    • 2014-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    相关资源
    最近更新 更多