123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- <template>
- <svg
- width="100%" height="100%"
- ref="svg" :id="id"
- >
- <template v-if="ready">
- <path
- v-for="(item, index) in links"
- :key="`link_${index}`"
- :d="lineGenerator([item.source, item.target])"
- :class="['link', item.linkType || item.target.data.linkType]"
- />
- <template v-for="(node, index) in nodes">
- <circle
- :key="'circle' + index"
- :cx="node.x"
- :cy="node.y"
- :class="node.data.class + (node.data.first ? ' first' : '')"
- :r="node.children ? 7 : 5"
- tabindex="0"
- @click="onNodeClick(node.data)"
- @keyup.enter="onNodeClick(node.data)"
- >
- <title>{{ node.data.title }}</title>
- </circle>
- <text
- v-if="showId"
- :key="'text' + index" :x="node.x + 7" :y="node.y-7"
- >{{ node.data.id }}</text>
- </template>
- </template>
- </svg>
- </template>
- <script>
- 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: {
- nodes: { type: Array, required: true },
- links: { type: Array, required: true },
- id: { type: String, default: 'node-map' },
- showId: { type: Boolean, default: true }
- },
- data () {
- return {
- ready: false,
- width: 100,
- height: 100,
- simulation: forceSimulation(),
- lineGenerator: line().x(node => node.x).y(node => node.y)
- }
- },
- methods: {
- updateSize () {
- const { width, height } = this.$el.getBoundingClientRect()
- Object.assign(this.$data, { width, height })
- },
- init () {
- this.updateSize()
- this.nodes.forEach(node => {
- this.$set(node, 'x', this.width * 0.5)
- this.$set(node, 'y', this.height * 0.5)
- })
- this.setupForces()
- // Wait for DOM update so d3 can select
- this.$nextTick(this.initListeners)
- this.ready = true
- },
- initListeners () {
- // TODO replace with vue events ?
- selectAll(`#${this.id} 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).id(d => d.id).distance(50))
- .force('charge', forceManyBody().strength(-150))
- .force('x', forceX())
- .force('y', forceY())
- .force('center', forceCenter(this.width * 0.5, this.height * 0.5))
- },
- // D3 DRAG EVENT HANDLERS
- 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
- },
- // BASIC HANDLERS
- onNodeClick (nodeData) {
- const { id, parents } = nodeData
- if (parents) {
- this.$emit('open', parents[0].id, id)
- } else {
- this.$emit('open', id)
- }
- }
- },
- async mounted () {
- this.init()
- }
- }
- </script>
- <style lang="scss" scoped>
- .link {
- stroke: grey;
- &.siblings {
- stroke-dasharray: 4;
- }
- &.parents {
- stroke: red;
- opacity: .3;
- }
- }
- text {
- font-size: 0.7rem;
- user-select: none;
- }
- @each $id in map-keys($families){
- .family-#{$id} {
- fill: setColorFromId($id);
- @if $id == 9 {
- stroke: black;
- }
- }
- }
- .first {
- stroke-width: 2px;
- }
- </style>
|