Reputation: 9076
I have adapted this force directed graph example from D3JS, https://observablehq.com/@d3/force-directed-graph. I am calling the code from with VueJS, so I have implemented it as a class.
The initial graph works fine, but when I try to add new data, it does not respond. As far as I can see, provided I can access the D3 edge and vertex selections, I should be able to call their join()
function at any time, feeding in new data, and that should automatically merge it. Perhaps I am mistaken?
The code below is reasonably long, but there are only two notable methods. Firstly, once the D3Diagram object has been instantiated, I call initialise()
, with arrays of edges and vertices. Later, when I want to add more data, I call the addMoreData()
method, with the new arrays.
import * as d3 from "d3";
export default class D3Diagram {
initialise(vertices, edges) {
this.vertices = vertices;
this.edges = edges;
this.createSvg();
this.createSimulation();
this.createEdgeSelection();
this.createVertexSelection();
this.configureSimulation();
}
createSvg() {
this.svg = d3.select('#diagram-wrapper')
.append('svg')
.attr('viewBox', [0, 0, 100, 100])
.classed('flex-grow-1', true);
}
createSimulation() {
this.simulation = d3.forceSimulation(this.vertices)
.force("link", d3.forceLink(this.edges).id(d => d.id))
.force("charge", d3.forceManyBody().strength(d => -4))
.force("center", d3.forceCenter(50, 50));
}
createEdgeSelection() {
this.edgeSelection = this.svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(this.edges)
.join("line");
this.edgeSelection.append("title").text(d => d.id);
}
createVertexSelection() {
this.vertexSelection = this.svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
.selectAll("circle")
.data(this.vertices)
.join("circle")
.attr("r", 2)
.attr("fill", color)
.call(drag(this.simulation));
}
configureSimulation() {
this.simulation.on('tick', () => {
this.edgeSelection
.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.vertexSelection
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
}
addMoreData(vertices, edges) {
this.vertexSelection.join(vertices);
this.edgeSelection.join(edges);
}
}
function color() {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return d => scale(d.group);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Upvotes: 0
Views: 758
Reputation: 7192
In D3, the data is bound to the selections. You do the binding with the data()
function.
If you want to update your selections, take your existing selection and bind data again. This creates a new selection, and the enter
, update
, and exit
selections of that new selection will contain the changed elements.
For example, changing data:
// Initial, empty selection.
this.vertexSelection = this.svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
// Bind the data once.
this.vertexSelection.data(data)
// Data join, creating new elements.
this.vertexSelection.join("line")
// Bind the data again.
this.vertexSelection.data(data)
// Data join. Will update, removing old data, appending new data,
// updating existing data.
this.vertexSelection.join("line")
You'll note that, by design, the calls when data is updated is always the same, save for initializing the selection. So we can put all of that in a render()
method that's idempotent: it'll work regardless of if this is the first call or the second, third and so on.. call.
Afterwards, you can split it up the same way you did in your current code. You can call init()
& render()
on component mount, to do the first render, and render()
whenever you have new data.
// pseudocode
function init() {
// Initial, empty selection.
this.vertexSelection = this.svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5)
}
function render(data) {
// Bind new data and do the data join.
this.vertexSelection
.data(this.data.vertexes)
.join("line")
}
mounted() {
this.init()
this.render()
}
// on data change... {
this.render()
// }
For more in-depth information, check out the selection.join() tutorial.
Upvotes: 1