NodeMap.vue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <template>
  2. <svg
  3. width="100%" height="100%"
  4. ref="svg" :id="id"
  5. >
  6. <template v-if="ready">
  7. <path
  8. v-for="(item, index) in links"
  9. :key="`link_${index}`"
  10. :d="lineGenerator([item.source, item.target])"
  11. stroke="black"
  12. />
  13. <template v-for="(node, index) in nodes">
  14. <circle
  15. :key="'circle' + index"
  16. :cx="node.x"
  17. :cy="node.y"
  18. :class="node.data.class + (node.data.first ? ' first' : '')"
  19. :r="node.children ? 7 : 5"
  20. tabindex="0"
  21. @click="onNodeClick(node.data)"
  22. @keyup.enter="onNodeClick(node.data)"
  23. >
  24. <title>{{ node.data.title }}</title>
  25. </circle>
  26. <text
  27. v-if="showId"
  28. :key="'text' + index" :x="node.x + 7" :y="node.y-7"
  29. >{{ node.data.id }}</text>
  30. </template>
  31. </template>
  32. </svg>
  33. </template>
  34. <script>
  35. import { line } from 'd3-shape'
  36. import { selectAll } from 'd3-selection'
  37. import { drag } from 'd3-drag'
  38. import {
  39. forceSimulation, forceLink, forceManyBody,
  40. forceX, forceY, forceCenter
  41. } from 'd3-force'
  42. export default {
  43. /*
  44. This component uses vue as the svg renderer and the rest is handle with d3 functions
  45. (events, data computing, etc.)
  46. */
  47. name: 'NodeMap',
  48. props: {
  49. nodes: { type: Array, required: true },
  50. links: { type: Array, required: true },
  51. id: { type: String, default: 'node-map' },
  52. showId: { type: Boolean, default: true }
  53. },
  54. data () {
  55. return {
  56. ready: false,
  57. width: 100,
  58. height: 100,
  59. simulation: forceSimulation(),
  60. lineGenerator: line().x(node => node.x).y(node => node.y)
  61. }
  62. },
  63. methods: {
  64. updateSize () {
  65. const { width, height } = this.$el.getBoundingClientRect()
  66. Object.assign(this.$data, { width, height })
  67. },
  68. init () {
  69. this.updateSize()
  70. this.nodes.forEach(node => {
  71. this.$set(node, 'x', this.width * 0.5)
  72. this.$set(node, 'y', this.height * 0.5)
  73. })
  74. this.setupForces()
  75. // Wait for DOM update so d3 can select
  76. this.$nextTick(this.initListeners)
  77. this.ready = true
  78. },
  79. initListeners () {
  80. // TODO replace with vue events ?
  81. selectAll(`#${this.id} circle`)
  82. .data(this.nodes)
  83. .call(drag()
  84. .on('start', this.onNodeDragStart)
  85. .on('drag', this.onNodeDrag)
  86. .on('end', this.onNodeDragEnd)
  87. )
  88. },
  89. setupForces () {
  90. this.simulation
  91. .nodes(this.nodes)
  92. .force('link', forceLink(this.links).id(d => d.id).distance(50))
  93. .force('charge', forceManyBody().strength(-150))
  94. .force('x', forceX())
  95. .force('y', forceY())
  96. .force('center', forceCenter(this.width * 0.5, this.height * 0.5))
  97. },
  98. // D3 DRAG EVENT HANDLERS
  99. onNodeDragStart (e, node) {
  100. if (!e.active) {
  101. this.simulation.alphaTarget(0.8).restart()
  102. }
  103. node.fx = node.x
  104. node.fy = node.y
  105. },
  106. onNodeDrag (e, node) {
  107. node.fx = e.x
  108. node.fy = e.y
  109. },
  110. onNodeDragEnd (e, node) {
  111. if (!e.active) {
  112. this.simulation.alphaTarget(0)
  113. }
  114. node.fx = null
  115. node.fy = null
  116. },
  117. // BASIC HANDLERS
  118. onNodeClick (nodeData) {
  119. const { id, parents } = nodeData
  120. if (parents) {
  121. this.$emit('open', parents[0].id, id)
  122. } else {
  123. this.$emit('open', id)
  124. }
  125. }
  126. },
  127. async mounted () {
  128. this.init()
  129. }
  130. }
  131. </script>
  132. <style lang="scss" scoped>
  133. path {
  134. stroke: grey;
  135. }
  136. text {
  137. font-size: 0.7rem;
  138. user-select: none;
  139. }
  140. @each $id in map-keys($families){
  141. .family-#{$id} {
  142. fill: setColorFromId($id);
  143. @if $id == 9 {
  144. stroke: black;
  145. }
  146. }
  147. }
  148. .first {
  149. stroke-width: 2px;
  150. }
  151. </style>