add NodeMap component that display an interactive text relation tree + test route
This commit is contained in:
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -10,6 +10,11 @@ const routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'map',
|
||||||
|
path: '/map',
|
||||||
|
component: () => import(/* webpackChunkName: "node-map" */ '../pages/Map')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'notfound',
|
name: 'notfound',
|
||||||
path: '/404',
|
path: '/404',
|
||||||
|
|||||||
Reference in New Issue
Block a user