Skip to content

Instantly share code, notes, and snippets.

@agnjunio
Last active August 10, 2018 15:24
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save agnjunio/c83d7a4b9e787385885f6e21de0ec8e7 to your computer and use it in GitHub Desktop.
Dependency Graph using D3 + Vue.js (1/3)
license: apache-2.0
height: 600

Based on:

The goal here is to have a Vue.js component that uses D3 to create a SVG Dependency Graph, with zoom and pan controls, and also other nice features.

In this first part, we will cover the basics of plotting a D3 Force Simualtion. You open the raw mode and use the Vue plugin to view and edit component data, like forces, nodes and links, and the component should update itself.

In Part 2, we will be adding some controls.

Summary:

{
"nodes": [
{ "name": "firmware", "group": 1, "class": "system" },
{ "name": "loader", "group": 1, "class": "system" },
{ "name": "kernel", "group": 1, "class": "system" },
{ "name": "systemd", "group": 1, "class": "mount" },
{ "name": "-.mount", "group": 1, "class": "mount" },
{ "name": "init.scope", "group": 1, "class": "init" },
{ "name": "system.slice", "group": 1, "class": "init" },
{ "name": "system-getty.slice", "group": 1, "class": "init" },
{ "name": "systemd-initctl.socker", "group": 1, "class": "init" },
{ "name": "tmp.mount", "group": 1, "class": "init" },
{ "name": "sys-devices", "group": 2, "class": "init" },
{ "name": "boot.mount", "group": 2, "class": "init" }
],
"links": [
{ "source": 1, "target": 0, "value": 1, "type": "depends" },
{ "source": 2, "target": 1, "value": 8, "type": "depends" },
{ "source": 3, "target": 2, "value": 6, "type": "depends" },
{ "source": 4, "target": 3, "value": 1, "type": "needs" },
{ "source": 5, "target": 3, "value": 1, "type": "needs" },
{ "source": 6, "target": 3, "value": 1, "type": "needs" },
{ "source": 7, "target": 3, "value": 1, "type": "needs" },
{ "source": 8, "target": 3, "value": 2, "type": "needs" },
{ "source": 9, "target": 3, "value": 1, "type": "needs" },
{ "source": 11, "target": 10, "value": 1, "type": "depends" },
{ "source": 11, "target": 3, "value": 3, "type": "depends" },
{ "source": 11, "target": 2, "value": 3, "type": "depends" },
{ "source": 11, "target": 3, "value": 5, "type": "needs" }
]
}
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment