NodeMap.vue 3.0 KB

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