|
@@ -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>
|