We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
2020 / 04 / 18
2022 / 08 / 03
DOM manipulation with D3 in Vue 3
Showcase the evolution of D3's general update pattern.
Prerequisites
Install the required packages:
pnpm add d3-selection d3-transition
pnpm add -D @types/d3-selection @types/d3-transition
How to import D3
You can import the whole of D3:
// Needs `pnpm add d3`
import * as d3 from 'd3'
// OR...
const d3 = await import('d3')
But the whole of D3 might contain functionality you won’t be using at the moment.
I think it’s better to install and import only the parts you know for sure you’ll use.
For this tutorial we only need d3-selection and
d3-transition.
Here’s how you can build a d3
object with limited functionality:
import * as d3Selection from 'd3-selection'
import * as d3Transition from 'd3-transition'
const d3 = Object.assign({}, d3Selection, d3Transition)
// OR...
const d3 = await Promise.all([
import('d3-selection'),
import('d3-transition'),
]).then((d3s) => Object.assign({}, ...d3s))
The general update pattern
The original way to update the DOM with D3 was usually met with a lot of confusion by developers around the globe. :dizzy:
Though it isn’t really a difficult concept. It just requires a little bit of mind bending. :grin:
Here, I’ll try to distill the main idea using a simple example written as a Vue component.
With Vue you’ll learn how to:
- Reference a DOM node and pass it to D3
With D3 you’ll learn how to:
- Make the initial data binding
- Update existing items
- Append entering items
- Remove exiting items
Here’s the source code for the demo at the beginning:
<script setup lang="ts">
import * as d3Selection from 'd3-selection'
import * as d3Transition from 'd3-transition'
import { shuffle } from 'lodash-es'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const d3 = Object.assign({}, d3Selection, d3Transition)
type MaybeInterval = ReturnType<typeof setInterval> | null
const containerRef = ref<HTMLElement | null>(null)
const interval = ref<MaybeInterval>(null)
onMounted(() => {
render()
interval.value = setInterval(() => render(), 3000)
})
onBeforeUnmount(() => {
if (interval.value) {
clearInterval(interval.value)
}
})
function randomLetters() {
return shuffle('abcdefghijklmnopqrstuvwxyz'.split(''))
.slice(0, Math.floor(5 + Math.random() * 22))
.sort()
}
function render() {
const container = d3.select(containerRef.value)
const selection = container
.selectAll('.letter')
.data(randomLetters(), (d) => d as string)
// Update
selection.attr('class', 'letter text-stone-700')
// Enter
selection
.enter()
.append('div')
.attr('class', 'letter text-lime-600')
.style('opacity', 0)
.style('transform', 'translateY(-28px)')
.text((d) => d)
.transition()
.duration(1500)
.style('opacity', 1)
.style('transform', 'translateY(0px)')
// Exit
selection
.exit()
.attr('class', 'letter text-red-600')
.transition()
.duration(1500)
.style('opacity', 0)
.style('transform', 'translateY(28px)')
.remove()
}
</script>
<template>
<div ref="containerRef" class="flex h-7 flex-wrap gap-2"></div>
</template>
Introducing join
I just found out about join, and it seems to simplify working with D3’s general update pattern by quite a lot.
Here’s an example with auto appending, updating and removing:
const container = d3.select(containerRef.value)
container
.selectAll('.item')
.data(this.randomLetters(), d => d)
.join('em')
.text(d => d)
.attr('class', 'item mx-1')
If you need more control on what happens when entering, updating and removing you can have that too.
Here is the code rewritten in the join style:
<script setup lang="ts">
import * as d3Selection from 'd3-selection'
import * as d3Transition from 'd3-transition'
import { shuffle } from 'lodash-es'
import { onBeforeUnmount, onMounted, ref } from 'vue'
const d3 = Object.assign({}, d3Selection, d3Transition)
type MaybeInterval = ReturnType<typeof setInterval> | null
const containerRef = ref<HTMLElement | null>(null)
const interval = ref<MaybeInterval>(null)
onMounted(() => {
render()
interval.value = setInterval(() => render(), 3000)
})
onBeforeUnmount(() => {
if (interval.value) {
clearInterval(interval.value)
}
})
function randomLetters() {
return shuffle('abcdefghijklmnopqrstuvwxyz'.split(''))
.slice(0, Math.floor(5 + Math.random() * 22))
.sort()
}
function render() {
const container = d3.select<d3Selection.BaseType, unknown>(containerRef.value)
const t = container.transition().duration(1500)
container
.selectAll('.letter')
.data(randomLetters(), (d) => d as string)
.join(
(enter) =>
enter
.append('div')
.attr('class', 'letter text-lime-600')
.style('opacity', 0)
.style('transform', 'translateY(-28px)')
.text((d) => d)
.call((enter) =>
enter
.transition(t)
.style('opacity', 1)
.style('transform', 'translateY(0px)')
),
(update) => update.attr('class', 'letter text-stone-700'),
(exit) =>
exit.call((exit) =>
exit
.attr('class', 'letter text-red-600')
.transition(t)
.style('opacity', 0)
.style('transform', 'translateY(28px)')
.remove()
)
)
}
</script>
<template>
<div ref="containerRef" class="flex h-7 flex-wrap gap-2"></div>
</template>
This doesn’t necessarily mean the general update pattern is deprecated or anything.
It just means we now have another —and most probably, better— way to do the same thing. :)
Resources
- D3 selection.join tutorial a more convenient and memorable API for joining data.
- D3 selection.join documentation Appends, removes and reorders elements as necessary to match the data that was previously bound by selection.data.