NodeMap.vue 3.2 KB

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