I am currently working on a Force-directed graph with d3JS. I am fairly new to d3 so I have based my (buggy) solution on tutorials and documentation.
Simply put I have an endpoint that supplies me with an object of node & link lists (in my code its a prop) periodically (every 5 seconds) and whenever I try to update my graph, I end up redrawing the entire graph on top of the old one.
Here is what I would like to achieve: A graph that dynamically adds, deletes & updates the graph without:
I am using NuxtJS v3 as my primary framework, although I think that's not too relevant.
Here is my implementation so far:
<template>
<div id="graph">
<svg id="graph-svg" width="960" height="600"></svg>
</div>
</template>
I access the svg using its id and use d3 to display the graph:
<script setup lang="ts">
import { ref,watch } from 'vue';
import * as d3 from 'd3';
const props = defineProps({
data: {
type: Object,
required: true,
},
});
const { data } = props;
const metaData = ref();
let nodes, edges, simulation, svg;
let link, node;
// Watch for changes in the data prop
watch(() => props.data, async (newVal) => {
if (newVal) {
// Update the metadata and nodes/edges arrays
metaData.value = props.data.graphStatistics;
nodes = Object.values(props.data.nodes);
edges = Object.values(props.data.edges);
// Initialize the graph
await initGraph();
}
}, { immediate: true });
// Initialize the graph
const initGraph = async () => {
// Select the SVG element and set its dimensions
svg = d3.select("#graph-svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight);
// Create the force simulation
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(edges).id((d: { id: any; }) => d.id).distance(300).strength(1))
.force("charge", d3.forceManyBody().strength(-7000))
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2))
.force("x", d3.forceX().strength(0.5))
.force("y", d3.forceY().strength(0.5))
.force("collide", d3.forceCollide(10));
// Append the links
link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(edges, (d) => d.id)
.join("line")
.attr("stroke-width", 1);
// Append the nodes
node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes, (d) => d.id)
.join("circle")
.attr("r", 10)
.attr("fill", "#537B87");
// Update the positions of the nodes and links on each tick of the simulation
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
};
</script>
It should also be noted that atm when the graph is redrawn it is instantiated in the top-left & not in the center.
Thanks in advance, I have completely given up & asking on here is my last option. I have been working on this for about a week now and still cannot figure it out. The other SO posts have brought me nowhere since they do not achieve a smooth updated graph.
Thanks again!!
So after some time I found a solution. I'm quite happy with the result & hopefully it can help some other person who is desperately trying to find a solution.
//initialize this at onMounted, no data is needed
initChart() {
this.svg= d3.select(this.$refs.svg)
.attr("viewBox", [-window.innerWidth / 2, -window.innerHeight / 2, window.innerWidth, window.innerHeight])//this will center the graph
.attr("width", window.innerWidth)
.attr("height", window.innerHeight);
this.link = this.svg.append("g")
.attr(...)//your styling
.selectAll("line");
this.node = this.chart.append("g")
.attr(...)//your styling
.selectAll("circle");
}
//create a method for the simulation ticks
ticked() {
this.link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
this.node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
//here is the ACTUAL updating
updateChart({nodes, edges}) {
// now this is probably the most important part, and comes directly from mike bostocks implementation
// Make a shallow copy to protect against mutation, while
// recycling old nodes to preserve position and velocity.
const old = new Map(this.node.data().map(d => [d.id, d]));
nodes = nodes.map(d => ({...old.get(d.id), ...d}));
edges = edges.map(d => ({...d}));
this.link = this.link
.data(edges, d => [d.source, d.target])
.join(enter => enter.insert("line", "circle")
.attr(...))//your styling
);
this.node = this.node
.data(nodes, d => d.id)
.join(enter => enter.append("circle")
.attr()//your styling
.call(node => node.append("title")// this is a tooltip
.text(d => d.name))
);
// this is also very important as it restarts the simulation so the graph actually looks like it is being updated in real time
this.simulation.nodes(nodes);
this.simulation.force("link").links(edges);
this.simulation.alpha(1).restart().tick();
this.ticked();
},
I hope this helps!