typescript - 使用 d3JS 动态更新力导向图
我目前正在使用 d3JS 制作一个 力导向 图。我对 d3 还比较陌生,所以我基于教程和文档构建了我的(有漏洞的)解决方案。
简单来说,我有一个端点,它定期(每5秒)向我提供一个节点和链接列表的对象(在我的代码中是一个属性),每当我尝试更新我的图表时,我都会在旧图表上重绘整个图表。
这是我想要实现的:一个能够动态添加、删除和更新图表的图表,而不需要:
- 重新绘制整个图表
- 对节点施加一些疯狂的力,使它们四处移动,即使模拟已经完成。(模拟应该只作用于更新后的节点)
我正在使用 NuxtJS v3 作为我的主要框架,尽管我认为这不是太相关。
这是我到目前为止的实现:
<template>
<div id="graph">
<svg id="graph-svg" width="960" height="600"></svg>
</div>
</template>
我通过它的 id 访问 svg,并使用 d3 来显示图表:
<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>
还应该注意的是,目前当图表被重绘时,它会在左上角实例化,而不是在中心。
提前感谢,我已经完全放弃了,在这里提问是我的最后选择。我已经为此工作了一周左右,但仍然无法解决问题。其他的 SO 帖子对我没有任何帮助,因为它们无法实现平滑更新的图表。
再次感谢!!
答案#1
所以过了一段时间,我找到了一个解决方案。我对结果相当满意,并希望它能帮助一些同样迫切想找到解决方法的人。
//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();
},
我希望这能帮到你!
