是否需要使用重复代码重新定义d3元素?

时间:2018-10-11 02:40:23

标签: d3.js

我对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>

1 个答案:

答案 0 :(得分: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变换,它使用现有节点的d.xd.y进行更新,因此更新代码比enter选择要紧凑得多。

完整演示:

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示例中包含很多冗余代码。

非常简短。