I am working on a temporal network graph, with pie charts as nodes. Along with the nodes/links changing over time, the pie charts are supposed to change. It works fine without incorporating the changing pie slices. When I am incorporating the changing slices I get this weird behaviour where the nodes/pies restart from their initial position every time the (time) slider moves, which makes the whole thing jitter rather severely.
Each nodes data looks like:
{
"id": "Mike",
"start": "2022-08-09",
"location": [12.34, -56.74],
"received": [{
"giver": "Susan",
"receiver": "Mike",
"user_timestamp": "2022-08-09",
"message": "thanks!",
"location": [3.1415, 9.6535]
}, {
"giver": "Joe",
"receiver": "Mike",
"user_timestamp": "2022-08-11",
"message": "so cool!",
"location": [27.18, 2.818]
}]
}
The received array holds all the data pertinent to the pie - each slice is the same size, and there as many slices as their are elements in the array. I am changing the pie slices by filtering the received array based on the user_timestamp
and the slider position. Another issue I'm having is that the pie slices are not updating properly when slices are added ...
rangeSlider.on("onchange", (val) => {
currentValue = Math.ceil(timeScale(val));
// this filters entire node/pie
const filteredNodes = userNetworkData.nodes.filter(
(d) => new Date(d.start) <= val
);
// filter the received array in each node
const filteredNodesReceived = filteredNodes.map((d) => {
return {
...d,
received: d.received.filter((r) => new Date(r.user_timestamp) <= val),
};
});
const filteredLinks = userNetworkData.links.filter(
(d) => new Date(d.start) <= val
);
// remove edge if either source or target is not present
const filteredLinksFiltered = filteredLinks.filter(
(d) => filteredNodesReceived.some(o => o.id == d.source.id) && filteredNodesReceived.some(o => o.id == d.target.id)
);
// point to new source, target structure
const filteredLinksMapped = filteredLinksFiltered.map((d) => {
return {
...d,
source: filteredNodesReceived.find(x => x.id==d.source.id),
target: filteredNodesReceived.find(x => x.id==d.target.id)
};
});
update(filteredNodesReceived, filteredLinksMapped);
});
The way its set up I am using userNetworkData to hold the data in some static version so I can bring it back after I have removed it. Maybe that doesn't make sense. I have tried updating the x,y,vx,vy each instance of userNetworkData.nodes
on slider change but the same jittering occurs.
filteredLinksMapped is my attempt to re-associate the links with the nodes (which now have a different amount of elements in the received array).
The relevant parts of update(nodes,links)
:
function update(nodes, links) {
node = node
.data(nodes, (d) => d.id)
.join(
(enter) => enter.append("g").call(drag(simulation))
);
paths = node
.selectAll("path")
.data(function (d, i) {
return pie(d.received);
})
.join(
(enter)=>
enter.append("svg:path")
.attr("class", "path")
.attr("d", arc)
.attr("opacity", 1)
// for making each pie slice visible
.attr("stroke", function (d) {
return color(d.data.receiver);
})
.attr("stroke-width", radius * 0.2)
.attr("fill", function (d, i) {
return color(d.data.giver);
})
.attr("cursor", "pointer")
.on("mousemove", function (event, d) {
//tooltips bit
div.transition().duration(200).style("opacity", 0.9);
// this bit puts the tooltip near the slice of the pie chart
div
.html(parse_message(d.data))
.style("left", event.pageX + 20 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", function (d) {
div.transition().duration(500).style("opacity", 0);
})
)
link = link
.data(links, (d) => [d.source, d.target])
.join("line")
.attr("stroke-width", radius * 0.3)
.attr("stroke", (d) => color(d.source.id));
simulation.nodes(nodes);
simulation.force("link").links(links,function(d){return d.id;});
simulation.alpha(0.5).tick();
simulation.restart();
ticked();
}
I am initializing my selections and simulation outside of update like so:
const simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
.force(
"link",
d3.forceLink().id((d) => d.id)
)
// .force("collide", d3.forceCollide().radius(2*radius ).iterations(3))
.force(
"y",
d3.forceY((d) => projection(d.location)[1])
)
.force(
"x",
d3.forceX((d) => projection(d.location)[0])
)
.on("tick", ticked);
let link = svg.append("g").attr("class", "links").selectAll("line");
let node = svg.append("g").attr("class", "nodes").selectAll("g");
Note I am forcing the nodes towards the coordinates corresponding to their lat/lon as I am moving towards transitioning between a "map" view and a network view.
Unfortunately i'm having trouble getting to work on codepen, i'll keep trying but hopefully that's enough.