一. d3中的选择器, 数据设置与 enter-update-exit 模式

173 阅读2分钟

选择器

选择单个元素(select)

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // @ts-ignore
  const selection = select(myDivRef.value).select('p'); // 选择myDivRef这个dom元素之下的p, 由于使用的是select方法,因此实际只会返回匹配到的第一个p
  selection.classed('colorRed', true) // 给选中的元素添加一个class: colorRed
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

选择所有匹配的元素(selectAll)

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p') // 选择myDivRef这个dom元素之下的所有p
  selection.classed('colorRed', true) // 给选择的元素加上一个class: colorRed
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

数据设置

设置单个数据(datum)

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).select('p')
  // 设置数据(数据源是单个数据)
  selection.datum({ name: '张三', age: 21 })
  // 设置匹配元素的显示内容,并给匹配元素添加一个class colorRed
  selection
    .text(
      (dataItem: any, idx: number) =>
        `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    )
    .classed('colorRed', true)
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

设置批量数据(data)

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是多个数据)
  selection.data([
    { name: '张三', age: 21 },
    { name: '李四', age: 22 },
  ])
  // 设置匹配元素的显示内容,并给匹配元素添加一个class colorRed
  selection
    .text(
      (dataItem: any, idx: number) =>
        `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    )
    .classed('colorRed', true)
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

数据与匹配元素的3种情况

数据数量与匹配元素数量一样

那么数据中的每一个会与元素中的每一个按照数据下标一一对应(如: 第一个数据元素, 对应第一个匹配的html元素)

此示例, 匹配的元素和数据都是两个

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是单个数据)
  selection.data([
    { name: '张三', age: 21 },
    { name: '李四', age: 22 },
  ])
  // 设置匹配元素的显示内容,并给匹配元素添加一个class colorRed
  selection
    .text((dataItem: any, idx: number) => {
      console.log(dataItem)
      return `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    })
    .classed('colorRed', true)
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

数据数量大于匹配元素数量

此示例, 数据两个, 匹配的元素一个

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是单个数据)
  selection.data([
    { name: '张三', age: 21 },
    { name: '李四', age: 22 },
  ])
  // 设置匹配元素的显示内容,并给匹配元素添加一个class colorRed
  selection
    .text((dataItem: any, idx: number) => {
      console.log(dataItem)
      return `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    })
    .classed('colorRed', true)
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

数据数量少于匹配元素数量

此示例数据为2, 匹配的元素为1, 此时会js会报错, 从结果可以推断出, selection.text(...)内部是按照匹配到的元素循环执行的, 并根据当前循环的数组下标从数据源的数组中获取数据, 如果没获取到数据, 那么text((dataItem)=>{...})中的dataItem将会是undefined

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是单个数据)
  selection.data([{ name: '张三', age: 21 }])
  // 设置匹配元素的显示内容,并给匹配元素添加一个class colorRed
  selection
    .text((dataItem: any, idx: number) => {
      console.log(dataItem)
      return `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    })
    .classed('colorRed', true)
  console.log(selection)
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

Enter, Update, Exit

概念

如果数组为 [3, 6, 9, 12, 15],将此数组绑定到三个 p 元素的选择集上。可以想象,会有两个数据没有元素与之对应,这时候 D3 会建立两个空的元素与数据对应,这一部分就称为 Enter。而有元素与数据对应的部分称为 Update。如果数组为 [3],则会有两个元素没有数据绑定,那么没有数据绑定的部分被称为 Exit。示意图如下所示。

image.png

获取update选集, enter选集,并进行相关操作

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是单个数据), 并返回 update选集
  const updateSelection = selection.data([
    { name: '张三', age: 21 },
    { name: '李四', age: 22 },
    { name: '王武', age: 23 },
    { name: '赵六', age: 24 },
  ])
  // 对 update选集 做更新操作
  updateSelection
    .text((dataItem: any, idx: number) => {
      console.log('update', dataItem, idx)
      return `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    })
    .classed('colorRed', true)
  console.log(updateSelection)
  // 获取 enter选集
  const enterSelection = updateSelection.enter()
  // 对enter选集做插入操作
  enterSelection.append('p').text((dataItem: any, idx: number) => {
    console.log('enter', dataItem, idx)
    return `新人:${dataItem.name}, 年纪:${dataItem.age}, 索引号:${idx}`
  })
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

从上图的结果中,可以得出以下结论

  • 对update选集适合执行直接用数据对匹配的html元素进行更新
  • 对enter选集适合先插入元素,再对插入的html元素进行更新

获取 exit选集, 并进行相应操作

<!--
@author: pan
-->

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { selectAll, select } from 'd3-selection'
const myDivRef = ref<HTMLDivElement>()
onMounted(() => {
  // 选择myDivRef下的第一个p
  // @ts-ignore
  const selection = select(myDivRef.value).selectAll('p')
  // 设置数据(数据源是单个数据), 并返回update选集
  const updateSelection = selection.data([
    { name: '张三', age: 21 },
    { name: '李四', age: 22 },
  ])
  // 对update选集做更新操作
  updateSelection
    .text((dataItem: any, idx: number) => {
      console.log('update', dataItem, idx)
      return `数据索引号:${idx}, 姓名:${dataItem.name}, 年龄:${dataItem.age}`
    })
    .classed('colorRed', true)
  console.log(updateSelection)
  // // 获取enter选集
  // const enterSelection = updateSelection.enter()
  // enterSelection.append('p').text((dataItem: any, idx: number) => {
  //   console.log('enter', dataItem, idx)
  //   return `新人:${dataItem.name}, 年纪:${dataItem.age}, 索引号:${idx}`
  // })
  // 获取exit选集
  const exitSelection = updateSelection.exit()
  console.log('exitSelection', exitSelection)
  // 如果先获取enter选集,再获取exit选集,那么会导致exit选集为空, 下面的text或remove代码就都无法执行
  exitSelection.text((itemData: any, idx: number) => {
    console.log('exit', itemData, idx)
    return `exit: ${idx}`
  })
  // exitSelection.remove()
})
</script>

<template>
  <p>我是第一个段落</p>
  <div ref="myDivRef">
    <p>我是div内部的第一个段落</p>
    <p>我是div内部的第二个段落</p>
    <p>我是div内部的第三个段落</p>
    <p>我是div内部的第四个段落</p>
  </div>
</template>

<style lang="scss" scoped>
.colorRed {
  color: red;
}
</style>

image.png

从上面代码的测试结果中,可以得出以下结论

  • 如果先获取enter选集,再获取exit选集,那么会导致exit选集为空, 下面的text或remove代码就都无法执行
  • exit选集适合做remove操作

参考文章

d3.js 入门学习记录(一) 选择器和数据绑定

D3.js:Update、Enter、Exit