|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<head> |
|
<script src="https://cdn.jsdelivr.net/npm/vue"></script> |
|
<script src="https://d3js.org/d3.v5.min.js"></script> |
|
</head> |
|
<body> |
|
<div id="app"> |
|
<dependency-graph></dependency-graph> |
|
</div> |
|
<script> |
|
Vue.config.devtools = true |
|
Vue.component('dependency-graph', { |
|
template: |
|
`<div :style="{ width: width + 'px', height: height + 'px' }"> |
|
<svg width="100%" height="100%"></svg> |
|
</div>`, |
|
data() { |
|
return { |
|
width: 1024, |
|
height: 768, |
|
selections: {}, |
|
simulation: null, |
|
forceProperties: { |
|
center: { |
|
x: 0.5, |
|
y: 0.5 |
|
}, |
|
charge: { |
|
enabled: true, |
|
strength: -300, |
|
distanceMin: 1, |
|
distanceMax: 2000 |
|
}, |
|
collide: { |
|
enabled: true, |
|
strength: .7, |
|
iterations: 1, |
|
radius: 35 |
|
}, |
|
forceX: { |
|
enabled: false, |
|
strength: .1, |
|
x: .5 |
|
}, |
|
forceY: { |
|
enabled: false, |
|
strength: .35, |
|
y: .5 |
|
}, |
|
link: { |
|
enabled: true, |
|
distance: 100, |
|
iterations: 1 |
|
} |
|
}, |
|
data: { |
|
nodes: [], |
|
links: [], |
|
}, |
|
} |
|
}, |
|
computed: { |
|
nodes() { return this.data.nodes; }, |
|
links() { return this.data.links; }, |
|
}, |
|
mounted() { |
|
this.simulation = d3.forceSimulation() |
|
.force("link", d3.forceLink()) |
|
.force("charge", d3.forceManyBody()) |
|
.force("collide", d3.forceCollide()) |
|
.force("center", d3.forceCenter()) |
|
.force("forceX", d3.forceX()) |
|
.force("forceY", d3.forceY()) |
|
.on("tick", this.tick) |
|
this.updateForces() |
|
|
|
this.selections.svg = d3.select(this.$el.querySelector("svg")) |
|
this.selections.graph = this.selections.svg.append("g") |
|
const graph = this.selections.graph |
|
|
|
// You can set data in any ways you want |
|
d3.json("data.json").then(data => { |
|
this.data = data |
|
}).catch(error => { |
|
console.error('Cannot proceed with simulation, failed to retrieve data.') |
|
}); |
|
}, |
|
methods: { |
|
tick() { |
|
const transform = d => { |
|
return "translate(" + d.x + "," + d.y + ")"; |
|
} |
|
|
|
const link = d => { |
|
return "M" + d.source.x + "," + d.source.y + " L" + d.target.x + "," + d.target.y; |
|
} |
|
|
|
const graph = this.selections.graph |
|
graph.selectAll("path").attr("d", link) |
|
graph.selectAll("circle").attr("transform", transform) |
|
graph.selectAll("text").attr("transform", transform) |
|
}, |
|
updateData() { |
|
this.simulation.nodes(this.nodes) |
|
this.simulation.force("link").links(this.links) |
|
|
|
const simulation = this.simulation |
|
const graph = this.selections.graph |
|
|
|
graph.selectAll("path") |
|
.data(simulation.force("link").links()) |
|
.enter().append("path") |
|
.attr("class", d => "link " + d.type) |
|
.exit().remove() |
|
|
|
graph.selectAll("circle") |
|
.data(simulation.nodes()) |
|
.enter().append("circle") |
|
.attr("r", 30) |
|
.attr("class", d => d.class) |
|
.exit().remove() |
|
|
|
graph.selectAll("text") |
|
.data(simulation.nodes()) |
|
.enter().append("text") |
|
.attr("x", 0) |
|
.attr("y", ".31em") |
|
.attr("text-anchor", "middle") |
|
.text(d => d.name) |
|
|
|
simulation.alpha(1).restart(); |
|
}, |
|
updateForces() { |
|
const { simulation, forceProperties, width, height } = this |
|
simulation.force("center") |
|
.x(width * forceProperties.center.x) |
|
.y(height * forceProperties.center.y); |
|
simulation.force("charge") |
|
.strength(forceProperties.charge.strength * forceProperties.charge.enabled) |
|
.distanceMin(forceProperties.charge.distanceMin) |
|
.distanceMax(forceProperties.charge.distanceMax); |
|
simulation.force("collide") |
|
.strength(forceProperties.collide.strength * forceProperties.collide.enabled) |
|
.radius(forceProperties.collide.radius) |
|
.iterations(forceProperties.collide.iterations); |
|
simulation.force("forceX") |
|
.strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) |
|
.x(width * forceProperties.forceX.x); |
|
simulation.force("forceY") |
|
.strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) |
|
.y(height * forceProperties.forceY.y); |
|
simulation.force("link") |
|
.distance(forceProperties.link.distance) |
|
.iterations(forceProperties.link.iterations); |
|
|
|
// updates ignored until this is run |
|
// restarts the simulation (important if simulation has already slowed down) |
|
simulation.alpha(1).restart(); |
|
} |
|
}, |
|
watch: { |
|
data: { |
|
handler(newData) { |
|
this.updateData() |
|
}, |
|
deep: true |
|
}, |
|
forceProperties: { |
|
handler(newForce) { |
|
this.updateForces() |
|
}, |
|
deep: true |
|
} |
|
} |
|
}) |
|
|
|
new Vue({ el: '#app' }) |
|
</script> |
|
<style> |
|
path.link { |
|
fill: none; |
|
stroke: #666; |
|
stroke-width: 1.5px; |
|
} |
|
path.link.depends { |
|
stroke: #005900; |
|
stroke-dasharray: 5, 2; |
|
} |
|
path.link.needs { |
|
stroke: #7f3f00; |
|
} |
|
|
|
circle { |
|
fill: #ffff99; |
|
stroke: #191900; |
|
stroke-width: 1.5px; |
|
} |
|
circle.system { |
|
fill: #cce5ff; |
|
stroke: #003366; |
|
} |
|
circle.mount { |
|
fill: #ffe5e5; |
|
stroke: #660000; |
|
} |
|
circle.init { |
|
fill: #b2e8b2; |
|
stroke: #001900; |
|
} |
|
|
|
text { |
|
font: 10px sans-serif; |
|
pointer-events: none; |
|
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; |
|
} |
|
</style> |
|
</body> |