瀏覽代碼

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

axolotle 3 年之前
父節點
當前提交
afca9e3de1
共有 3 個文件被更改,包括 189 次插入0 次删除
  1. 145 0
      src/components/NodeMap.vue
  2. 39 0
      src/pages/Map.vue
  3. 5 0
      src/router/index.js

+ 145 - 0
src/components/NodeMap.vue

@@ -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 - 0
src/pages/Map.vue

@@ -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 - 0
src/router/index.js

@@ -10,6 +10,11 @@ const routes = [
     path: '/',
     component: Home
   },
+  {
+    name: 'map',
+    path: '/map',
+    component: () => import(/* webpackChunkName: "node-map" */ '../pages/Map')
+  },
   {
     name: 'notfound',
     path: '/404',