NodeMap.vue 3.4 KB

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