Recently I am trying to understand D3 Chord graphs, especially with the D3v7. I got two different datasets and want to dynamically update those sets. In general it works beside two problems:
The first object in each datasets will be ignored. I do not why as any console.log shows the existing data but it is simply not drawn. For example dataset 1 based on groups1 and paths1 is missing object A and in dataset2 object E is missing.
As soon as the graph gets updated the old elements are still there and the graph creates new paths without removing the old ones. Each click and change increases the D3 elements in the browser, instead replacing them.
Since D3v3 and D3v7 a bit changed and I can´t figure out quickly enough to help myself.
////////////////////////////////////////////////////////////
////////////////////// Set-Up Data /////////////////////////
////////////////////////////////////////////////////////////
let groups1 = [
{ name: "A", color: "blue" },
{ name: "B", color: "red" },
{ name: "C", color: "green" },
{ name: "D", color: "yellow" },
]
let paths1 = [
{ from: groups1.name = "A", to: groups1.name = "A", strength: 0 },
{ from: groups1.name = "A", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "A", to: groups1.name = "C", strength: 5 },
{ from: groups1.name = "A", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "B", strength: 0 },
{ from: groups1.name = "B", to: groups1.name = "C", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "C", strength: 0 },
{ from: groups1.name = "C", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "D", strength: 0 },
{ from: groups1.name = "D", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "C", strength: 5 },
]
let groups2 = [
{ name: "E", color: "#301E1E" },
{ name: "F", color: "#083E77" },
{ name: "G", color: "#342350" },
]
let paths2 = [
{ from: groups2.name = "E", to: groups2.name = "E", strength: 0 },
{ from: groups2.name = "E", to: groups2.name = "F", strength: 5 },
{ from: groups2.name = "E", to: groups2.name = "G", strength: 5 },
{ from: groups2.name = "F", to: groups2.name = "E", strength: 5 },
{ from: groups2.name = "F", to: groups2.name = "F", strength: 0 },
{ from: groups2.name = "F", to: groups2.name = "G", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "E", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "F", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "G", strength: 0 },
]
let chord = []
let matrix = []
function getMatrix(paths, groups) {
matrix = []
var mapPaths = paths.map(item => {
const container = {}
container.from = groups.findIndex(ele => ele.name == item.from)
container.to = groups.findIndex(ele => ele.name == item.to)
container.strength = item.strength
return container
})
mapPaths.forEach(function (item) {
// initialize sub-arra if not yet exists
if (!matrix[item.to]) {
matrix[item.to] = []
}
matrix[item.to][item.from] = item.strength
})
return matrix
}
// --- --- --- --- --- ---
const vw = document.documentElement.clientWidth / 2
const vh = document.documentElement.clientHeight / 2
let innerRadius = Math.min(vw, vh) * 0.5
let outerRadius = innerRadius * 1.1;
let svg = d3.select("#chart").append("svg")
.attr("width", vw)
.attr("height", vh)
.attr("overflow", "unset")
var chordGenerator = d3.chord()
.padAngle(0.10)
.sortSubgroups(d3.ascending)
.sortChords(d3.descending)
chord = chordGenerator(getMatrix(paths1, groups1))
var arc = d3.arc()
.innerRadius(innerRadius * 1.01)
.outerRadius(outerRadius)
var ribbon = d3.ribbon()
.radius(innerRadius);
window.update = update;
update(paths1, groups1)
function update(thisPaths, thisGroups) {
let groups = thisGroups
let paths = thisPaths
getMatrix(thisPaths, thisGroups)
const chords = chordGenerator(matrix);
let wrapper = svg.append("g")
.attr("transform", "translate(" + vw / 2 + "," + vh / 2 + ")")
let ribbons = wrapper.append("g");
let g = wrapper.selectAll("g")
.data(chord.groups)
.enter()
.append("g")
.attr("class", "group")
const ribbonsUpdate = ribbons.selectAll("path")
.data(chords, ({ source, target }) => source.index + '-' + target.index)
const duration = 1000;
ribbonsUpdate
.transition()
.duration(duration)
.attr("d", ribbon)
.style("fill", function (d) { return groups[d.target.index].color; })
ribbonsUpdate
.enter()
.append("path")
.attr("opacity", 0)
.attr("d", ribbon)
.style("fill", function (d) { return groups[d.target.index].color; })
.transition()
.duration(duration)
.attr('opacity', 1)
// adding Arc Blocks
g.append("path")
.style("fill", function (d) { return groups[d.index].color; })
.attr("d", arc)
.style("opacity", 1)
// adding labels
g.append("text")
.each(function(d){ d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", ".35em")
.attr("class", "titles")
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + (outerRadius + 10) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.text(function(d,i){ return groups[i].name; })
ribbonsUpdate
.exit()
.transition()
.duration(duration)
.attr("opacity", 0)
.remove();
}
document.getElementById('data1').onclick = () => {
chord = chordGenerator(getMatrix(paths1, groups1))
update(paths1, groups1);
}
document.getElementById('data2').onclick = () => {
chord = chordGenerator(getMatrix(paths2, groups2))
update(paths2, groups2);
}
body {
font-size: 16px;
font-family: 'Oswald', sans-serif;
text-align: center;
background-color: #ECF0F3;
cursor: default;
overflow: hidden;
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Animated_D3v7</title>
<!-- jQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<!-- D3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" charset="utf-8"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<style>
body {
font-size: 16px;
font-family: 'Oswald', sans-serif;
background-color: #ECF0F3;
cursor: default;
overflow: hidden;
}
</style>
<body>
<button id="data1">data 1</button>
<button id="data2">data 2</button>
<div id="chart"></div>
</body>
</html>
Problem 1 is solved by:
let g = wrapper.selectAll("g.group") // add class of 'group' to the selector
There's some ambiguity with the selection otherwise because you have already added a g
to wrapper
in the previous statement: let ribbons = wrapper.append("g");
Problem 2 seems a bit more involved.
Firstly, every time update
is called a new g
is created under svg
and assigned to wrapper
. You can define wrapper
outside of update
just the once.
Secondly, update
uses both chord
and chords
where the former is updated outside of update
and the latter within. You can dispense with the former and just update chord
and matrix
within update
.
Thirdly, you can set-up a g
for each of arcs, ribbons and labels outside and then the enter, update and exit logic works against those path
and text
elements within them.
I've simplified a few other little bits in the working example below:
const vw = document.documentElement.clientWidth / 2
const vh = document.documentElement.clientHeight / 2
const innerRadius = Math.min(vw, vh) * 0.4
const outerRadius = innerRadius * 1.1;
const duration = 1000;
const svg = d3.select("#chart").append("svg")
.attr("width", vw)
.attr("height", vh)
const wrapper = svg.append("g")
.attr("transform", "translate(" + vw / 2 + "," + vh / 2 + ")")
const ribbonsG = wrapper.append("g").attr("id", "ribbons")
const arcsG = wrapper.append("g").attr("id", "arcs")
const labelsG = wrapper.append("g").attr("id", "labels")
const chordGenerator = d3.chord()
.padAngle(0.10)
.sortSubgroups(d3.ascending)
.sortChords(d3.descending)
const arc = d3.arc()
.innerRadius(innerRadius * 1.01)
.outerRadius(outerRadius)
const ribbon = d3.ribbon()
.radius(innerRadius);
update(paths1, groups1)
function getMatrix(paths, groups) {
matrix = []
var mapPaths = paths.map(item => {
const container = {}
container.from = groups.findIndex(ele => ele.name == item.from)
container.to = groups.findIndex(ele => ele.name == item.to)
container.strength = item.strength
return container
})
mapPaths.forEach(function (item) {
// initialize sub-arra if not yet exists
if (!matrix[item.to]) {
matrix[item.to] = []
}
matrix[item.to][item.from] = item.strength
})
return matrix
}
function update(thisPaths, thisGroups) {
const groups = thisGroups
const paths = thisPaths
const chords = chordGenerator(getMatrix(thisPaths, thisGroups));
// ribbons
const ribbonsUpdate = ribbonsG
.selectAll("path.ribbon")
.data(chords, ({ source, target }) => source.index + '-' + target.index)
const ribbonsEnter = ribbonsUpdate
.enter()
.append("path")
ribbonsUpdate
.merge(ribbonsEnter)
.attr("opacity", 0)
.attr("class", "ribbon")
.transition()
.duration(duration)
.attr("d", ribbon)
.style("fill", function (d) { return groups[d.target.index].color; })
.attr('opacity', 1)
ribbonsUpdate
.exit()
.transition()
.duration(duration)
.attr("opacity", 0)
.remove();
// arcs
const arcsUpdate = arcsG
.selectAll("path.arc")
.data(chords.groups)
const arcsEnter = arcsUpdate
.enter()
.append("path")
arcsUpdate
.merge(arcsEnter)
.attr("opacity", 0)
.attr("class", "arc")
.transition()
.duration(duration)
.attr("d", arc)
.style("fill", function (d) { return groups[d.index].color; })
.attr('opacity', 1)
arcsUpdate
.exit()
.transition()
.duration(duration)
.attr("opacity", 0)
.remove();
// adding labels
const labelsUpdate = labelsG
.selectAll("text.titles")
.data(chords.groups)
const labelsEnter = labelsUpdate
.enter()
.append("text")
labelsUpdate
.merge(labelsEnter)
.attr("class", "titles")
.attr("opacity", 0)
.transition()
.duration(duration)
.each(function(d){ d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + (outerRadius + 10) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.text(function(d,i){ return groups[i].name; })
.attr("opacity", 1)
labelsUpdate
.exit()
.remove()
}
document.getElementById('data1').onclick = () => {
update(paths1, groups1);
}
document.getElementById('data2').onclick = () => {
update(paths2, groups2);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" charset="utf-8"></script>
<div id="chart"></div>
<button id="data1">data 1</button>
<button id="data2">data 2</button>
<script>
let groups1 = [
{ name: "A", color: "blue" },
{ name: "B", color: "red" },
{ name: "C", color: "green" },
{ name: "D", color: "yellow" },
]
let paths1 = [
{ from: groups1.name = "A", to: groups1.name = "A", strength: 0 },
{ from: groups1.name = "A", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "A", to: groups1.name = "C", strength: 5 },
{ from: groups1.name = "A", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "B", strength: 0 },
{ from: groups1.name = "B", to: groups1.name = "C", strength: 5 },
{ from: groups1.name = "B", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "C", to: groups1.name = "C", strength: 0 },
{ from: groups1.name = "C", to: groups1.name = "D", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "D", strength: 0 },
{ from: groups1.name = "D", to: groups1.name = "A", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "B", strength: 5 },
{ from: groups1.name = "D", to: groups1.name = "C", strength: 5 },
]
let groups2 = [
{ name: "E", color: "#301E1E" },
{ name: "F", color: "#083E77" },
{ name: "G", color: "#11AA99" },
]
let paths2 = [
{ from: groups2.name = "E", to: groups2.name = "E", strength: 0 },
{ from: groups2.name = "E", to: groups2.name = "F", strength: 5 },
{ from: groups2.name = "E", to: groups2.name = "G", strength: 5 },
{ from: groups2.name = "F", to: groups2.name = "E", strength: 5 },
{ from: groups2.name = "F", to: groups2.name = "F", strength: 0 },
{ from: groups2.name = "F", to: groups2.name = "G", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "E", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "F", strength: 5 },
{ from: groups2.name = "G", to: groups2.name = "G", strength: 0 },
]
</script>