add NodeMap component that display an interactive text relation tree + test route

This commit is contained in:
axolotle
2021-02-24 17:35:14 +01:00
parent d46ed9dea8
commit afca9e3de1
3 changed files with 189 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
<template>
<svg width="100%" height="100%" ref="svg">
<path
v-for="(item, index) in links"
:key="`link_${index}`"
:d="lineGenerator([item.source, item.target])"
stroke="black"
/>
<circle
v-for="(node, index) in nodes"
:key="index"
:cx="node.x"
:cy="node.y"
:fill="getNodeFill(node)"
:r="5"
>
<title>{{ node.data.name }}</title>
</circle>
</svg>
</template>
<script>
import { hierarchy } from 'd3-hierarchy'
import { line } from 'd3-shape'
import { selectAll } from 'd3-selection'
import { drag } from 'd3-drag'
import {
forceSimulation, forceLink, forceManyBody,
forceX, forceY, forceCenter
} from 'd3-force'
export default {
/*
This component uses vue as the svg renderer and the rest is handle with d3 functions
(events, data computing, etc.)
*/
name: 'NodeMap',
props: {
data: { type: Object, required: true }
},
data () {
return {
width: 100,
height: 100,
h: hierarchy({}),
simulation: forceSimulation(),
lineGenerator: line().x(node => node.x).y(node => node.y)
}
},
computed: {
nodes () {
return this.h.descendants()
},
links () {
return this.h.links()
}
},
methods: {
updateSize () {
const { width, height } = this.$el.getBoundingClientRect()
Object.assign(this.$data, { width, height })
},
init () {
this.updateSize()
const h = hierarchy(this.data)
h.descendants().forEach(node =>
Object.assign(node, {
x: this.width * 0.5,
y: this.height * 0.5
})
)
this.h = h
this.setupForces()
// Wait for DOM update so d3 can select
this.$nextTick(this.initListeners)
},
initListeners () {
// TODO replace with vue events ?
selectAll('circle')
.data(this.nodes)
.call(drag()
.on('start', this.onNodeDragStart)
.on('drag', this.onNodeDrag)
.on('end', this.onNodeDragEnd)
)
},
setupForces () {
this.simulation
.nodes(this.nodes)
.force('link', forceLink(this.links).distance(100))
.force('charge', forceManyBody().strength(-350))
.force('x', forceX())
.force('y', forceY())
.force('center', forceCenter(this.width * 0.5, this.height * 0.5))
},
getNodeFill (node) {
return node.data.type === 'textref' ? 'red' : 'black'
},
// DRAG EVENT HANDLER
onNodeDragStart (e, node) {
if (!e.active) {
this.simulation.alphaTarget(0.8).restart()
}
node.fx = node.x
node.fy = node.y
},
onNodeDrag (e, node) {
node.fx = e.x
node.fy = e.y
},
onNodeDragEnd (e, node) {
if (!e.active) {
this.simulation.alphaTarget(0)
}
node.fx = null
node.fy = null
}
},
async mounted () {
this.init()
}
}
</script>
<style lang="scss" scoped>
</style>
+39
View File
@@ -0,0 +1,39 @@
<template>
<div id="map">
<node-map v-if="data" :data="data" />
<div>
{{ data }}
</div>
</div>
</template>
<script>
import NodeMap from '@/components/NodeMap'
export default {
name: 'Home',
components: {
NodeMap
},
data () {
return {
text: null,
data: null
}
},
created () {
this.$store.dispatch('GET_TREE', 17).then((data) => {
this.data = data
})
}
}
</script>
<style lang="scss" scoped>
#map {
height: 100%;
width: 100%;
}
</style>
+5
View File
@@ -10,6 +10,11 @@ const routes = [
path: '/',
component: Home
},
{
name: 'map',
path: '/map',
component: () => import(/* webpackChunkName: "node-map" */ '../pages/Map')
},
{
name: 'notfound',
path: '/404',