D3选集(Selection)解析-附API文档翻译

2,058 阅读31分钟

选集(selection)是D3的基础,它用于选择元素,并支持编辑、绑定数据等操作。为了自己的巩固和分享,写了这篇解析D3选集的文章。

阅读本文,需要您学习过D3的基础。如果错误,感谢指正。

解析

成员结构

我们首先来看看选集的内部成员结构,以这样的3级元素结构为例:

<div class="level-1">
  <div class="level-2">
    <div class="level-3"></div>
  </div>
  <div class="level-2">
    <div class="level-3"></div>
    <div class="level-3"></div>
  </div>
</div>
<div class="level-1">
  <div class="level-2">
    <div class="level-3"></div>
    <div class="level-3"></div>
    <div class="level-3"></div>
  </div>
</div>

如果我们只选择第一级元素:

let selection = D3.selectAll('.level-1')

打印结果我们可以看到:

{
    _groups:[NodeList]
    _parents:[HTML]
}

这里用元素类型来指代对象,以示意。

_groups和_parents都是数组,_groups的内容元素是NodeList,内部数组的个数与参与选择的父元素有关,即有多少个元素“调用”了selectAll方法,那么_groups里就有多少个NodeList,同时这些父元素也会出现在_parents中。

NodeList是一个类数组对象,包含两个.level-1元素。

在这里直接使用D3调用selectAll相当从根元素(html)开始查找,需要_parents属性中有一个HTML元素。

我们接下来选择第二级元素:

let selection = D3.selectAll('.level-1').selectAll('.level-2')

因为D3采用链式调用,这里相当于:

let level1 = D3.selectAll('.level-1')
let selection = level1.selectAll('.level')

这里参与选择的有刚才选出来的两个.level-1元素,所以打印结果我们可以看到:

{
    _groups:[NodeList, NodeList],
    _parents:[HTMLDIVElement, HTMLDIVElement]
}

_groups里有两个NodeList,分别包含从两个.level-1元素中选出来的.level-2元素,分成两组。不管被选出来的子元素在多深的层级中,都被放到同一个NodeList中。参与选择的两个.level-1元素被添加到_parents属性中。

第一个NodeList对象中有2个.level-2元素,第二个NodeList对象中有1个.level-2元素。

接着我们选择第三级元素:

let selection = D3.selectAll('.level-1').selectAll('.level-2').selectAll('.level-3')

结果:

{
    _groups:[NodeList, NodeList, NodeList],
    _parents:[HTMLDIVElement, HTMLDIVElement, HTMLDIVElement]
}

因为上一次被选择的.level-2元素有3个(无论分组),所以这里有3组选出来的元素。第一个NodeList有1个.level-3元素,第二个NodeList都有2个.level-3元素,第三个NodeList有3个.level-3元素。

遍历

选集是Selection类对象,它有很多操作元素的方法,比如attrclassedstyle,调用它们是所有分组中的所有元素都会被操作,那么这个遍历的过程是怎样的呢?

这些方法通常可以接受一个函数作为参数,并传入d(绑定的数据),i(索引),g(分组)作为参数。

我们以each方法来举例:

selection.each((d, i, g) => {
  console.log(d, i, g)
})

打印结果:

undefined 0 NodeList
undefined 0 NodeList(2)
undefined 1 NodeList(2)
undefined 0 NodeList(3)
undefined 1 NodeList(3)
undefined 2 NodeList(3)

括号中的数字代表内容元素的个数。

因为我们还没有绑定数据,所以d都是undefined。从打印结果看,each方法是一个组一个组的遍历。

编辑

有些方法对被选择的元素和数量(_groups成员的内容)进行编辑,比如filtermerge。那么在编辑后,会对元素产生怎样的影响呢?

filter

对于filter其实很好理解,它是对每个分组各自进行操作,与Arrayfilter方法类似。

原本selection_groups成员的结构是:

[
    [element],
    [element, element],
    [element, element, element]
]

此处用元素的类示意div元素。

let filtered = selection.filter(':nth-child(odd)')

:nth-child(odd)选择器表示选择奇数索引(从1开始)的元素,得到的结果:

[
    [element],
    [element],
    [element, element]
]

selection3个分组的元素数量分别是1、2、3,所以过滤的结果符合预期。

merge

merge方法用于将选集和另一个选集合并。

我们这里新增一个选集extra

[    [extra, extra],
    [extra],
    []
]

selection执行合并操作:

let merged = selection.merge(extra)

得到结果:

[
    [element],
    [element, element],
    [element, element, element]
]

结果与selection选集没有区别,因为merge方法不是简单的“相加”,它的真正作用是用来填充原本选集中不存在(或是nullundefined)的部分。

举个例子,如果selection选集是这样的:

[
    [null],
    [undefined, element],
    [empty, element, element]
]

这里的empty表示数组的[0]不存在。

extra选集是这样的:

[    [extra, extra],
    [extra],
    []
    [extra]
]

执行合并操作:

let merged = selection.merge(extra)

会得到结果:

[
    [extra]
    [extra, element]
    [empty, element, element]
]

可以看到,selection选集原本为nullundefined的位置,如果extra选集中相同位置有元素,那么被填充到selection选集中;如果extra中也没有,那么selection中相应位置保持为空。同时,如果extra有额外的分组,也不会被添加到selection中。

至于为什么会有空位,这得看数据绑定了。

绑定数据

如果我们有这样的元素:

<div class="element"></div>
<div class="element"></div>
<div class="element"></div>

和这样的数据:

let data = [0, 1, 2]

为了让元素显示数据中的数字,我们进行数据绑定的操作:

let update = selection.data(data)

data方法返回一个新的选集,内部结构是这样的:

{
    _groups:[element, element, element],
    _enter:[%empty%, %empty%, %empty%],
    _exit:[%empty%, %empty%, %empty%]
}

注意:这里的%empty%表示没有元素,之所以不表示为空数组,是因为虽然实际上是空数组,但是数组的length属性为3。

我们首先来看看_groups中的元素,它就是我们之前选择的元素,其__data__属性上被注入了相对应的数据,按顺序一个元素对应一个数字。

我们可以让D3帮我们把数字显示到元素上:

update.text(d => d)

enter

如果数据变多了,比如:

data = [0, 1, 2, 3]
update = update.data(data)

update变为:

{
    _groups:[element, element, element, %empty%]
    _enter:[%empty%, %empty%, %empty%, EnterNode],
    _exit:[%empty%, %empty%, %empty%]
}

可以看到_groups数组的长度变为4,第4个元素是空的。_enter数组长度变为4,前3个元素依然为空,但是第4个元素是EnterNode,代表需要加入的新元素,也正是_groups缺少的第4个元素。因为4个数据需要4个DOM元素展示,但是现在只有3个。

所以我们会调用enter方法来选择所有需要新增的结点(用“结点”这个词是因为此时还没有创建DOM元素):

let enter = update.enter()

enter选集的_groups属性为:

[%empty%, %empty%, %empty%, EnterNode]

EnterNode虽然不是DOM元素,但是它也有__data__属性,也绑定了数据。

接着我们就可以创建div元素并显示数字了:

enter
  .append('div')
  .classed('element', true)
  .text(d => d)

exit

如果数据变少了,比如:

data = [0, 1]
update = update.data(data)

update变为:

{
    _groups:[element, element]
    _enter:[%empty%, %empty%],
    _exit:[%empty%, %empty%, element]
}

可以看到_groups数组的长度变为2,少了1个元素。_exit第3个元素是非空,且是一个存在的DOM元素,代表需要删除的新元素,也正是_groups多出来的第3个元素。2个数据只需要2个DOM元素展示,现在多了1个。

所以我们会调用exit方法来选择所有需要删除的元素:

let exit = update.exit()

exit选集的_groups属性为:

[%empty%, %empty%, %empty%, element]

然后我们调用remove方法删除元素:

exit.remove()

join

数据的变化需要修改元素来变更展示,所以我们通常将这一过程封装成一个函数,比如叫draw函数。有时候数据的变化时不确定的,我们不知道数据变多还是变少,我们得把enterexit过程结合起来。

示例:

function draw(data) {
  let update = D3.select(document.body).selectAll('.element').data(data)

  update
    .enter()
    .append('div')
    .classed('element', true)
    .text(d => d)

  update.exit().remove()
}

分别运行draw([1, 2, 3])draw([1, 2, 3, 4])draw([1, 2])这3中情况,可以看到页面内显示了和数据对应的元素。

但我们如果运行draw([1, 2, 3])draw([2, 3, 4]),我们发现元素的数字没有展示234,依然是123。这是因为在这样的情况下,D3比较是否要更新只是根据元素和数据数量的差异。这里数据的数量没有变,那么元素的个数就满足条件,所以没有新增或删除元素。

这里还有一个问题,就是我们只是在新增元素的时候设置了元素的文案,但是如果数据变了,原本已经存在的元素的文本是不会自动更新的。所以我们要把draw函数修改为:

function draw(data) {
  let update = D3.select(document.body).selectAll('.element').data(data)

  update
    .enter()
    .append('div')
    .classed('element', true)
    .text(d => d)

  update.exit().remove()

  update.text(d => d)
}

这样,元素就正确的更新了。我们也可以依靠merge方法,将enterupdate选集合并,有之前的讨论我们可以知道,两个选集的长度相同且在元素上是互补的关系。这样做的好处是我们只用写一遍text方法调用的逻辑:

function draw(data) {
  let update = D3.select(document.body).selectAll('.element').data(data)

  let enter = update.enter().append('div').classed('element', true)

  update.merge(enter).text(d => d)

  update.exit().remove()
}

这就是D3中最常见的一种更新逻辑了。因为它太常见了,D3在新版本中加入了join方法来简化这样的操作。

使用join方法可以把draw函数简化为:

function draw(data) {
  D3.select(document.body)
    .selectAll('.element')
    .data(data)
    .join('div')
    .classed('element', true)
    .text(d => d)
}

join方法可以接受一个元素名称作为参数,也可以接受3个函数作为参数,这3个函数分别传递了enterupdateexit选集。上面的写法等价于:

function draw(data) {
  D3.select(document.body)
    .selectAll('.element')
    .data(data)
    .join(
      enter => enter.append('div'),
      update => update,
      exit => exit.remove()
    ).classed('element', true)
    .text(d => d)
}

需要注意的是,传入enter的函数需要返回新增的元素的选集(例子中是新创建的div元素选集),传入update的函数需要返回更新的选集,而传入exit的函数不需要返回值。因为前两个返回值会被join方法merge并返回,以供进一步的处理。

自定义方法

通过修改Selection原型可以添加自定义方法来方便我们的编程,比如一次修改多个属性的方法:

D3.selection.prototype.attrs = function (attributes) {
  let keys = Object.keys(attributes)
  for (let key of keys) {
    this.attr(key, attributes[key])
  }
}

D3.selectAll('div').attrs({
  title: 'div',
  style: 'color: blue'
})

参考资料

API翻译

基于版本:6。

选集提供强大的数据驱动DOM变换能力,包括设置属性、样式、成员属性、HTML或文本内容等。使用联结数据的enter和exit选集,可以根据数据来添加或移除元素。

选集方法通常返回当前选集或这一个新的选集。通过方法链,可以简单地在给定的选集上进行一系列操作。比如,设置类名或document落元素的颜色样式:

d3.selectAll("p")
    .attr("class", "graf")
    .style("color", "red");

等同于:

var p = d3.selectAll("p");
p.attr("class", "graf");
p.style("color", "red");

按照惯例,返回当前选集的方法前置4个空格的缩进,而返回新选集的方法前置2个空格的缩进。这有利于表明上下文的不同,因为它们在方法链中很突出:

d3.select("body")
  .append("svg")
    .attr("width", 960)
    .attr("height", 500)
  .append("g")
    .attr("transform", "translate(20,20)")
  .append("rect")
    .attr("width", 920)
    .attr("height", 460);

选集是不可变的。所有选择元素或改变顺序的选集方法都返回一个新的选集,而不是修改当前选集。但是,元素是可变的,因为选集驱动document的变化。

API参考

  • Selecting Elements
  • Modifying Elements
  • Joining Data
  • Handling Events
  • Control Flow
  • Local Variables
  • Namespaces

选择元素

选集方法接受selector字符串比如.fancy作为参数来选择处具有fancy类的元素,或是div作为参数来选择出div标签元素。选集方法有两种形式:select和selectAll,前一种选择出第一个匹配的元素,后一种选择出以document顺序匹配到的所有元素。两个顶级方法——d3.select和d3.selectAll查询整个document,子集方法——selection.select和selection.selectAll,将选择范围限制在被选元素的后代。

# d3.selection()

选择根元素,document.documentElement。这个方法可以用来测试对象是否是选集(instanceof d3.selection)或是扩展选集原型。比如,添加一个方法来检查多选框:

d3.selection.prototype.checked = function(value) {
  return arguments.length < 1
      ? this.property("checked")
      : this.property("checked", !!value);
};

然后这样使用:

d3.selectAll("input[type=checkbox]").checked(true);

# d3.select(selector)

根据指定的selector字符串选择第一个匹配到的元素。如果没有元素匹配selector字符串,返回一个空选集。如果多个元素匹配selector字符串,只有第一个匹配的元素(以document顺序)会被选择。比如,选择第一个锚点元素:

var anchor = d3.select("a");

如果selector不是一个字符串,那么选择给定的结点。在已经有了一个结点的引用的情况下很有用,比如事件监听器中的this或全局document.body。比如,让被点击的段落变成红色:

d3.selectAll("p").on("click", function() {
  d3.select(this).style("color", "red");
});

# d3.selectAll(selector)

选择所有和selector字符串匹配的元素,被选择的元素以document顺序(从顶至底)排列。如果document中没有元素匹配selector,或者selector是null或undefined,那么返回一个空选集。比如,选择所有段落:

var paragraph = d3.selectAll("p");

如果selector不是字符串,那么选择给定的一组结点,这在已经有了一些结点的引用时很有用,比如事件监听器中的this.childNodes或全局的document.links。这些结点可以是伪数组,像是NodeListarguments。比如,把所有链接的颜色设置为红色:

d3.selectAll(document.links).style("color", "red");

# selection.select(selector)

对于每一个被选择的元素,选择第一个匹配selector的后代元素。如果当前元素没有后代元素匹配给定的selector,那么返回的选集中,相应索引位置为null(如果selector是null,返回的选集中的每个元素都是null,即返回一个空选集)。如果当前元素有关联的数据,数据被传递给对应的被选择元素。如果多个元素匹配selector,只有以document顺序的第一个元素被选择。比如,选择每个段落中的第一个粗体元素:

var b = d3.selectAll("p").select("b");

如果selector是一个函数,它会按顺序对被选择的元素执行,传递当前数据(d)、当前索引(i)和当前结点组(nodes)作为参数,并且this指向当前的DOM元素(nodes[i])。它必须返回一个元素,或者没有元素匹配的话,返回null。比如选择每一个段落的前一个兄弟结点:

var previous = d3.selectAll("p").select(function() {
  return this.previousElementSibling;
});

# selection.selectAll(selector)

对于每一个被选择的元素,选择每一个匹配selector字符串的后代元素。返回的选集中,被选择的元素依据它们在选集中的祖先元素来分组,如果没有元素匹配给定的selector或是selector是null,那么当前索引的分组为空。 被选择的元素不继承从选集来的数据。使用selection.data把数据传递给子元素。比如,选择每一个段落的每一个粗体元素:

var b = d3.selectAll("p").selectAll("b");

如果selector是一个函数,它按顺序对每一个被选择的元素执行,传递当前数据(d)、当前索引(i)和当前分组(nodes)作为参数,其中this指向当前dom元素(nodes[i])。它必须返回一个数组的元素(或者伪数组,比如NodeList),如果没有匹配元素的话,返回一个空数组。比如,选择每一个段落的前一个和后一个兄弟元素:

var sibling = d3.selectAll("p").selectAll(function() {
  return [
    this.previousElementSibling,
    this.nextElementSibling
  ];
});

与selection.select不同的是,selection.selectAll不影响分组,每一个被选择的元素依据它在原本选集中的祖先元素来分组。分组在数据联结中很重要。

# selection.filter(filter)

过滤选集,返回一个新选集,仅包含过滤器为true的元素。filter可以是选择器字符串或函数。如果filter是一个函数,它会按顺序对每一个元素执行,传递当前数据(d)、当前索引(i)和当前分组(nodes)作为参数,其中this指向当前DOM元素(nodes[i])。

比如,筛选表格选集中的偶数行:

var even = d3.selectAll("tr").filter(":nth-child(even)");

这约等于直接使用d3.selectAll,但是索引可能不同:

var even = d3.selectAll("tr:nth-child(even)");

类似的,可以使用函数:

var even = d3.selectAll("tr").filter(function(d, i) { return i & 1; });

或者是使用selection.select:

var even = d3.selectAll("tr").select(function(d, i) { return i & 1 ? this : null; });

注意:nth-child伪类的索引是从1开始而不是从0开始。所以,上面的筛选函数和:nth-child的意义并不完全一样。它们取决于选集索引而不是DOM中前导兄弟元素的数量。

返回的过滤选集会保留原选集的祖先,但是和array.filter一样,它不保留那些被移除的元素的索引。如果有需要,可以使用selection.select来保留索引。

# selection.merge(other)

返回一个将当前选集和给定选集合并的选集。返回的选集和当前选集有相同的分组数量和祖先元素。选集中的任何空位(null)会被给定选集中相应的存在的元素填充。(如果other中有额外的分组或祖先元素,它们会被忽略。)

这个方法常用来在数据联结后合并enter和update选集。在修改enter元素和update元素后,可以将两个选集合并,避免写两份代码来执行同一个操作。例如:

var circle = svg.selectAll("circle").data(data) // UPDATE
    .style("fill", "blue");

circle.exit().remove(); // EXIT

circle = circle.enter().append("circle") // ENTER
    .style("fill", "green")
  .merge(circle) // ENTER + UPDATE
    .style("stroke", "black");

如果想完全理解这些代码,可以查阅selection.data方法,它通常被称作更新模式。

这个方法不是用来结合任意的选集的,但是,如果当前选集和给定的选集在相同索引有非空元素,那么当前选集的元素会被返回而另一个选集的元素被忽略。

# d3.matcher(selector)

对于给定的selector,返回一个函数,这个函数中,如果this指向的元素和给定的selector匹配,那么返回true。这个方法通常在selection.filter内部使用。比如,这样:

var div = selection.filter("div");

等价于:

var div = selection.filter(d3.matcher("div"));

虽然D3不是兼容层,但由于最近element.matches的标准化,这种实现也只支持浏览器前缀实现。

# d3.selector(selector)

对于给定的selector,返回一个函数,这个函数返回匹配给定selector的this指向元素的第一个子元素。这个方法通常用作selection.select的内部方法。比如:

var div = selection.select("div");

等价于:

var div = selection.select(d3.selector("div"));

# d3.selectorAll(selector)

对于给定的selector,返回一个函数,这个函数返回匹配给定selector的this指向元素的所有元素。这个方法通常用作selection.selectAll的内部方法。比如:

var div = selection.selectAll("div");

等价于:

var div = selection.selectAll(d3.selectorAll("div"));

# d3.window(node)

返回拥有给定node的window对象。如果node是一个结点,返回拥有者的document默认视图;如果node是一个document,返回它的默认视图;否则返回自身。

# d3.style(node,name)

返回给定的node的给定名称的样式属性值。如果node具有给定名称name的内联属性,那么返回这个属性值;否则返回计算属性值。另见selection.style。

修改元素

选择元素之后,使用选集的变换方法来影响document内容。比如,设置锚点元素的名称属性和颜色样式:

d3.select("a")
    .attr("name", "fred")
    .style("color", "red");

# selection.attr(name[, value])

如果给定了value,那么设置被选择元素的name属性为value,并返回这个选集。如果value是一个常数,所有元素被赋予相同属性值;如果value是一个函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。函数的返回值用来设置每一个元素的属性。value为null会移除给定的属性。

如果没有给定value,返回选集中第一个非空元素的的给定名称name的属性值。如果你知道选集中只包含一个元素,一般这会很有用。

给定的name可以具有命名空间,比如xlink:href来指定href属性所属的XLink命名空间。详情另见命名空间以查看所有支持的命名空间。可以通过注册来添加额外的命名空间。

# selection.classed(names[, value])

如果给定了value,设置或清除被选择元素的给定的CSS类名,返回当前选集。这个方法是通过设置class属性或修改classList属性来工作的。给定的名称names可以是空格分隔的字符串。比如,给选择的元素设置foo和bar类名:

selection.classed("foo bar", true);

如果value是真值,那么所有元素都被赋予这个类名;否则,类名会被清除。如果value是一个函数,那么它会按顺序对每一个被选择的元素执行,传入当前数据(d)、当前索引(i)和当前分组(nodes),其this指针指向当前DOM元素(nodes[i])。然后函数的返回值被用来设置或清除每个元素的类名。比如,随机的给被选择的元素的一半设置foo类名:

selection.classed("foo", function() { return Math.random() > 0.5; });

如果没有给定value,当且仅当第一个非空元素有给定的类名时返回true。这在你知道选集中只有一个元素时很有用。

# selection.style(name[, value[, priority]])

如果给定value,设置被选择元素的给定名称name的样式属性为value,并且返回当前选集。如果value是一个常数,那么所有元素都被设置为给定的样式属性值;否则,如果value是一个函数,它会按顺序对每个元素执行,传入当前数据(d)、当前索引(i)和当前分组(nodes),其this指针指向当前DOM元素(nodes[i])。函数的返回值用来设置每个元素的样式属性值。null值会移除样式属性。可选属性priority可以设置为null或是字符串important(不需要感叹号)。

如果没有给定value,返回选集里第一个非空元素的给定样式属性的值。如果元素的内联值存在,那么返回这个值,否在返回计算值。取得当前样式值在当前选集只有一个元素的时候很有用。

警告:与许多SVG属性不同,CSS样式一般有相关的单位。比如,3px是一个有效的线宽属性值,但是3不是。一些浏览器会隐含的添加px(像素)给数值,但并不是所有浏览器都会这么做:比如IE,它会抛出“无效参数”的错误。

# selection.property(name[, value])

一些HTML元素有不能使用attribute或style获取的属性,比如form元素的域文本value和多选框的checked布尔值。可以使用这个方法来获取或设置这些属性。

如果给定了value,那么设置被选择元素的name属性为value,并返回这个选集。如果value是一个常数,所有元素被赋予相同属性值;如果value是一个函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。函数的返回值用来设置每一个元素的属性。value为null会移除给定的属性。

如果没有给定value,返回选集中第一个非空元素的的给定名称name的属性值。如果你知道选集中只包含一个元素,一般这会很有用。

# selection.text([value])

如果给定了value,那么设置被选择元素的内容为value,并返回这个选集。如果value是一个常数,所有元素被赋予相同的内容值;如果value是一个函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。函数的返回值用来设置每一个元素的内容。value为null会移除内容。

如果没有给定value,返回选集中第一个非空元素的的内容。如果你知道选集中只包含一个元素,一般这会很有用。

# selection.html([value])

如果给定了value,那么设置被选择元素的innerHTML为value,并返回这个选集。如果value是一个常数,所有元素被赋予相同的innerHTML;如果value是一个函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。函数的返回值用来设置每一个元素的内容。value为null会移除innerHTML。

如果没有给定value,返回选集中第一个非空元素的的innerHTML。如果你知道选集中只包含一个元素,一般这会很有用。

应当使用selection.append或selection.insert来创建数据驱动的内容,而这个方法是用来在需要一点HTML时使用的,比如富文本样式。同事selection.html仅支持HTML元素。SVG元素和其它非HTML元素不支持innerHTML属性,所以它们无法使用selection.html。考虑使用XMLSerializer把一个DOM子树转换为文本。另见innersvg.polyfill,它提供一个让SVG元素支持innerHTML属性的方法。

# selection.append(type)

如果给定的type是一个字符串,给选集中的每一个元素在尾端添加一个这个类型(标签名)的子元素。如果选集是一个enter选集,那么插入到update选集的下一个兄弟元素前。enter选集的这一种方式让你可以根据新绑定的数据来插入元素到DOM中。但是需要注意的是,如果正在更新的元素改变了顺序,那么仍然需要调用selection.order。

如果value是一个函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。这个函数应当返回一个元素来添加到DOM中。(方法通常新建一个元素,但也可能返回一个存在的元素。)比如,往每一个段落插入一个DIV元素:

d3.selectAll("p").append("div");

这等价于:

d3.selectAll("p").append(function() {
  return document.createElement("div");
});

同时也等价于:

d3.selectAll("p").select(function() {
  return this.appendChild(document.createElement("div"));
});

两种情况中,这个方法都返回一个包含新添加元素的新选集。每一个新元素继承当前元素的数据,和selection.select一样。

给定的name可以具有命名空间前缀,比如svg:text用于声明一个SVG命名空间中的text属性。另见命名空间以查看所有支持的命名空间,也可以注册额外的命名空间。如果没有指明命名空间,会从父元素继承。或者,如果name是一个已知的前缀,那么相应的命名空间会被使用。(比如,svg隐含意味着svg:svg

# selection.insert(type[, before])

如果给定的type是一个字符串,那么在对于每一个匹配给定的before选择器的被选择元素,在它们各自之前插入一个这个类型(标签名)的新元素。比如,如果before选择器是:fitst-child,那么元素会被插入到第一个元素之前。如果没有给定before,它默认为null。(想要按照和绑定数据一致的顺序插入元素,应当使用selection.append)

type和before都可以是函数,这个函数按顺序对所有元素执行,传递当前数据(d),当前索引(i),当前分组(nodes)为参数,this指向当前DOM元素(nodes[i])。type函数应该返回一个待插入的元素。before函数应该返回应当被作为插入参照的子元素,新元素会插入到这个元素之前。比如,插入一个DIV元素到每一个段落:

d3.selectAll("p").insert("div");

这等价于:

d3.selectAll("p").insert(function() {
  return document.createElement("div");
});

同时也等价于:

d3.selectAll("p").select(function() {
  return this.insertBefore(document.createElement("div"), null);
});

两种情况中,这个方法都返回一个包含新添加元素的新选集。每一个新元素继承当前元素的数据,和selection.select一样。

给定的name可以具有命名空间前缀,比如svg:text用于声明一个SVG命名空间中的text属性。另见命名空间以查看所有支持的命名空间,也可以注册额外的命名空间。如果没有指明命名空间,会从父元素继承。或者,如果name是一个已知的前缀,那么相应的命名空间会被使用。(比如,svg隐含意味着svg:svg

# selection.remove()

从document中删除被选择的元素,返回这个现在被从DOM中分离的选集(被删除的元素)。目前这不是一个用来把删除的元素重新添加回document的API。但是,可以传递一个函数到selection.append或selection.insert来重新添加元素。

# selection.clone([deep])

复制被选择的元素并立即插入到本体之后,返回新添加的克隆体。如果deep是真值,被选择元素的子元素也会被复制。否则,只有被选择的元素会被复制。等价于:

selection.select(function() {
  return this.parentNode.insertBefore(this.cloneNode(deep), this.nextSibling);
});

# selection.order(compare)

将元素重新插入document中,使得每一组的文档顺序和选集相同。如果数据时已经排序好的,这个方法等价于调用selection.sort,但是更快。

# selection.raise()

按顺序重新插入被选择的元素,作为它们父元素的最后一个子元素。等价于:

selection.each(function() {
  this.parentNode.appendChild(this);
});

# selection.lower()

按顺序重新插入被选择的元素,作为它们父元素的第一个子元素。等价于:

selection.each(function() {
 this.parentNode.insertBefore(this, this.parentNode.firstChild);
});

# d3.create(name)

对于给定的元素名,返回一个只有一个元素的选集,其中包含了给定名称的元素,且它脱离的当前的document。

# d3.creator(name)

对于给定的元素名,返回一个用于创建元素的函数,假定this指向父元素。这个方法常用于selection.append和selection.insert内部,来创建新元素。比如:

selection.append("div");

等价于:

selection.append(d3.creator("div"));

另见命名空间以查看支持的命名空间前缀详情。

联结数据

# selection.data([data[, key]])

联结给定的数据数组和选择的元素,返回一个表示更新选集的新选集:此时数据已经成功的与元素绑定。同时,enter和exit选集也被返回,它们可以用来添加和删除与新数据对应的元素。给定的data可以是任意值得数组(比如数字或对象),或者是给每个分组返回一个数组的函数。当数据被赋予一个元素时,它被存储在__data__属性中,使得数据与元素“粘连”,在重新选择时可以获取。

数据被分配到选集中的每个分组。如果选集有多个分组(比如调用d3.selectAll之后调用selection.selectAll),那data通常赋予一个函数。这个函数会按顺序对每个分组执行,被传递分组父元素的数据(d,或是undefined),组索引(i)和选集父元素结点(nodes),其中this指向分组的父元素。比如,创建一个矩阵的HTML表格:

var matrix = [
  [11975,  5871, 8916, 2868],
  [ 1951, 10048, 2060, 6171],
  [ 8010, 16145, 8090, 8045],
  [ 1013,   990,  940, 6907]
];

var tr = d3.select("body")
  .append("table")
  .selectAll("tr")
  .data(matrix)
  .enter().append("tr");

var td = tr.selectAll("td")
  .data(function(d) { return d; })
  .enter().append("td")
    .text(function(d) { return d; });

这个例子中,data函数是一个恒等函数,对于表格每一行,它返回数据矩阵中的对应行。

如果key没有给定,那么data中的第一个数据被赋予第一个被选择的元素,第二个数据被赋予第二个被选择的元素,如此类推。可以给定一个key函数来控制哪一个数据被赋予哪一个元素,它会计算一个字符串类型的标识符,一次来标识数据和元素,代替默认的依据所以联结数据的方式。key函数会按顺序对每个被选择的元素执行,被传递当前数据(d),当前索引(i)和当前分组(nodes)为参数,其中this指向当前的DOM元素(nodes[i])。返回的字符串表示元素的键值。key函数之后也同样对data中的每个数据执行,传入当前数据(d)、当前索引(i)和分组的新数据,其中this指向分组的父元素DOM元素。返回的字符串是数据的键值。一个给定key的数据被赋予到匹配key的元素。如果多个元素有相同的key,多余的元素被放入exit选集;如果多个数据有相同的key,多余的数据被放入enter选集。

比如,有这样的文档

<div id="Ford"></div>
<div id="Jarrah"></div>
<div id="Kwon"></div>
<div id="Locke"></div>
<div id="Reyes"></div>
<div id="Shephard"></div>

你可以这样通过key来联结数据:

var data = [
  {name: "Locke", number: 4},
  {name: "Reyes", number: 8},
  {name: "Ford", number: 15},
  {name: "Jarrah", number: 16},
  {name: "Shephard", number: 31},
  {name: "Kwon", number: 34}
];

d3.selectAll("div")
  .data(data, function(d) { return d ? d.name : this.id; })
    .text(function(d) { return d.number; });

这个key函数的例子使用数据d(如果存在),否则退一步使用元素的id属性。因为这些元素之前没有绑定过数据,所以当key函数在被选择元素上执行时,数据d为空;在新元素上执行时,d非空。

返回的update和enter选集按数据排序,而exit选集保留联结之前的顺序。如果给定了key函数,选集中元素的顺序可能与文档中不同,可能需要调用selection.order或selection.sort。

虽然数据联结可以很方便地根据数据创建(或追加)一组元素,但更常见的情况下,数据联结是根据需要用来创建、销毁和更新元素的,使得DOM和新数据保持一致。数据联结可以高效的完成这些工作,只需要在每种状态的元素上执行必要的操作(添加,更新,或去除)。同时,状态之间的转换可以由简单地动画。以下是一个一半更新模式的例子:

var circle = svg.selectAll("circle") // 1
  .data(data) // 2
    .style("fill", "blue"); // 3

circle.exit().remove(); // 4

circle = circle.enter().append("circle") // 5, 9
    .style("fill", "green") // 6
  .merge(circle) // 7
    .style("stroke", "black"); // 8

可以分为几个步骤:

  1. 选择存在的circle元素(svg选集的子元素)。
  2. circle元素联结数据,返回update选集。
  3. 更新选集中的圆设置蓝色填充。
  4. 任何不匹配新数据的现存circle元素(exit选集)被去除。
  5. 对于任何不匹配现存circle元素的新数据(enter选集),添加新的circle元素。
  6. 这些新添加的circle元素设置绿色填充。
  7. 创建一个新选集,表示新添加和更新的circle元素的并集。
  8. 这些新添加和更新的circle元素设置黑色的边框。
  9. 这些circle元素保存在变量circle中。

如前一段叙述的那样,匹配逻辑是由传递到selection.data的key函数来决定的。因为上面的例子里没有用到key函数,那么元素和数据时依靠索引来联结的。

如果没有给定数据,方法返回选择元素的数据数组。

这个方法不能用来清除绑定的数据,用selection.datum代替。

# selection.enter()

返回enter选集,其中的元素是没有对应DOM元素的用来给数据占位的元素。(除了selection.data返回的选集,enter选集是空的。)

enter选集通常是用来还没根据新数据而创建的“不存在”的元素。比如,从一组数字创建DIV元素:

var div = d3.select("body")
  .selectAll("div")
  .data([4, 8, 15, 16, 23, 42])
  .enter().append("div")
    .text(function(d) { return d; });

如果body最初是空的,上面的代码会创建6个新的DIV元素,把它们按顺序添加到body中,然后把文本内容赋予相联结(转换为字符串)的数字:

<div>4</div>
<div>8</div>
<div>15</div>
<div>16</div>
<div>23</div>
<div>42</div>

概念上来说,enter选集的占位元素是指向父元素(在这个离子中是document.body)的指针。enter选集一般只在添加元素时暂时使用,且常常在被添加后与update选集合并。这样,对元素的修改可以同时作用于enter和update选集。

# selection.exit()

返回exit选集:现存的选集中没有新数据的元素。(除了selection.data返回的选集,exit选集是空的。)

exit选集通常是用来去除与旧数据相联结的“多余的”元素。比如,为了更新之前由一组数字创建的元素:

div = div.data([1, 2, 4, 8, 16, 32], function(d) { return d; });

因为给定了key函数(作为标识函数),并且新数据包含与文档中元素匹配的数字只有 [4, 8, 16],所以update选集只包含3个元素。保留那些元素,可以使用enter选集来添加[1, 2, 32]的新元素:

div.enter().append("div").text(function(d) { return d; });

同样的,去除现存的[15, 23, 42]元素:

div.exit().remove();

现在文档是这样的:

<div>1</div>
<div>2</div>
<div>4</div>
<div>8</div>
<div>16</div>
<div>32</div>

DOM元素的顺序和数据的顺序相同,因为旧数据的顺序和新数据的顺序是一致的。如果新数据的顺序不同,使用selection.order来重新调整DOM中的元素。

# selection.datum([value])

设置或获取被选择元素的绑定数据。不像selection.data,这个方法 不计算联结且不影响索引或者enter和exit选集。

如果给定value,设置所有被选择元素的绑定数据为给定的数据。如果value是一个常数,所有元素被赋予相同的数据。否则,如果value是一个函数,它会按顺序对选集中的每一元素执行,传入当前数据(d),当前索引(i),和当前分组(nodes),其中this指向当前DOM元素(nodes[i])。这个函数通常用来设置元素的新数据,传入null值会删除绑定数据。

如果没有给定value,返回第一个非空元素的的绑定数据。这在已知选集中只包含一个元素的时候很有用。

这个方法对于获取HTML5的自定义数据属性时非常有用。比如,假如有下面的元素:

<ul id="list">
  <li data-username="shawnbot">Shawn Allen</li>
  <li data-username="mbostock">Mike Bostock</li>
</ul>

你可以通过设置元素的数据为内置的dataset属性来把自定义数据属性暴露出来:

selection.datum(function() { return this.dataset; })

处理事件

为了交互,选集允许监听和触发事件。

# selection.on(typenames[, listener[, capture]])

为每一个元素添加或移除给定事件名称的监听器。typenames须是事件类型的字符串,比如clickmouseoversubmit,可以使用任意浏览器支持的事件类型。类型名称可以跟随一个.和一个名字,这个名字允许多个回调函数注册到同一个类型的事件上,比如click.fooclick.bar。如果要给定多个类型名,用空格分隔,比如input change或者click.foo click.bar

当一个被选择元素触发了一个事件时,给定的监听器会在这个元素上执行,传入当前数据(d),当前索引(i)和当前分组(nodes)作为参数,同时this指向当前DOM元素(nodes[i])。数据总是最新的,但是索引是选集的一个属性并且在监听器赋值时固定了。想要更新索引,需要重新赋值监听器。想要在监听器中获取事件对象,需要使用d3.evemt。

当一个事件监听器先前已经注册到选择元素的同一个事件名上,那么旧监听器在新监听器被添加之前会被移除。想要移除一个监听器,传递null给listener。想要移除给定名称上的所有监听器,传递null给listener并传递.foo给typename,其中foo是名称。移除没有名字的所有监听器,传递.给typename。

可以给定一个可选的capture标志位,这个标志位的含义与W3C中的一致:“在初始化捕获后,所有给定类型的事件会先被发送到注册的事件监听器,再被发送到它们下面的事件目标结点。沿着树向上冒泡的事件不会触发设定为使用捕获的事件监听器。”

如果没有给定listener,返回第一个非空的被选择元素的给定名称事件的当前的监听器。如果给定了多个事件名称,返回第一个匹配的监听器。

# selection.dispatch(type[, parameters])

按顺序发送一个给定类型的自定义事件到每一个被选择的元素。可以设置一个可选的参数映射作为事件的额外属性。它可以包含以下字段:

  • bubbles - 如果是true,事件从叶子结点发送到根节点。
  • cancelable - 如果是true,event.preventDefault有效。
  • detail - 与事件相关的自定义数据。

如果parameters是一个函数,它按顺序对每一个选择的元素执行,传入当前数据(d),当前索引(i)和当前分组(nodes)作为参数,同时this指向当前DOM元素(nodes[i])。它必须返回当前元素的参数映射。

# d3.event

当前事件。当事件监听器执行时被设置,监听器执行结束时重置。可以使用它来获取标准的事件字段,比如event.timeStamp,和方法,比如event.preventDefault。你也可以使用原生的event.pageX和event.pageY,有利于转换事件位置为相对于容器对象的本地事件位置,使用d3.mouse、d3.touch或d3.touches来接收事件对象。

如果你使用Balbel、Webpack或是其他ES6到ES5打包器,注意在一个事件过程中,d3.event的值可能改变。d3.event的引入必须是动态绑定,所以可能需要设置打包器来从D3的ES6模块引入,而不是从生成的UMD打包。不是所有打包器都观察jsnext:main。同样也要注意和全局window.event的冲突。

# d3.customEvent(event, lisener[, that[, arguments]])

调用给定的监听器listener,使用给定的that作为this上下文并且传入给定的arguments(如果有的话)。在调用时,d3.event被设定成给定的event。listener返回(或抛出错误)后,d3.event恢复为原来的值。另外,设置event.sourceEvent为d3.event的前一个值,这样自定义事件可以保留对原本原生事件的引用。返回listener返回的值。

# d3.mouse(container)

返回相对于给定容器的当前事件的x和y坐标。容器可以是HTML或SVG容器元素,比如G元素或是SVG元素。坐标以两个数字元素的数组[x, y]的的形式返回。

# d3.touch(container[, touches], identifier)

返回相对于给定的容器的,具有与当前事件相关的给定标识的触摸的x和y坐标。容器可以是一个HTML或SVG容器元素,比如G元素或SVG元素。坐标以两个数字元素的数组[x, y]的的形式返回。如果触摸中没有拥有给定identifier的,那么返回null。这对于忽略仅有一些触摸移动时的触摸事件很有用。如果没有给定touches,它默认为当前事件的changedTouches属性。

# d3.clientPoint(container, event)

返回相对于给定容器的给定事件的x和y坐标。(事件可能是一个触摸。)容器可以是HTML或SVG容器元素,比如G元素或是SVG元素。坐标以两个数字元素的数组[x, y]的的形式返回。

控制流

在更高级的应用中,选集提供自定义控制流的方法。

# selection.each(function)

按顺序对每一个选择的元素调用给定的函数,传入当前数据(d)、当前索引(i)和当前分组(nodes)作为参数,同时this指向当前的DOM元素(nodes[i])。这个方法可以用来对每一个选择的元素调用任意的代码,而在同时获取父元素和子元素的数据时非常有用,比如:

parent.each(function(p, j) {
  d3.select(this)
    .selectAll(".child")
      .text(function(d, i) { return "child " + d.name + " of " + p.name; });
});

# selection.call(function[, arguments...])

调用给定的函数一次,传入选集和可选的参数。返回这个选集。这和手动调用function等价,但是提供方法链。比如,在可复用的函数中设置多个样式:

function name(selection, first, last) {
  selection
      .attr("first-name", first)
      .attr("last-name", last);
}

现在可以这样:

d3.selectAll("div").call(name, "John", "Snow");

这差不多等于:

name(d3.selectAll("div"), "John", "Snow");

唯一的区别是selection.call总是返回这个选集但是不反悔被调用函数name。

# selection.empty()

如果选集没有元素的话,返回true。

# selection.nodes()

以数组的形式返回选集中的所有元素。

# selection.node()

返回选集中的第一个非空元素。如果选集是空的,返回null。

# selection.size()

返回选集中元素的总数。

局部变量

D3允许定义与数据无关的局部变量。比如,当渲染较小但是多个事件序列数据时,可能需要一个相同的x轴比例但是多个不同的y轴比例来比较每个矩阵的相对表现。D3局部变量局限在DOM元素内:一旦设定,数值存储在给定的元素中,获取时,只从给定的元素或最近的定义这个变量的祖先元素中获取。

# d3.local()

声明一个新的本地变量。比如:

var foo = d3.local();

var类似,每一个局部变量是一个唯一的引用符号,但与var不同的是,每一个局部变量的值都局限在DOM元素中。

# local.set(node, value)

设定给定结点上的这个局部变量为value,返回给定的value。这个方法通常用selection.each调用:

selection.each(function(d) { foo.set(this, d.value); });

如果你只是想单单设置一个变量,考虑使用selection.property:

selection.property(foo, function(d) { return d.value; });

# local.get(node)

返回给定结点的局部变量的值。如果节点莫有定义这个局部变量,返回最近祖先元素中定义了这个局部变量的值。如果祖先元素中没有定义这个局部变量,返回undefined。