Browse Source

update NodeMap to display DotButton and add zoom/pan

axolotle 3 years ago
parent
commit
3d28a54f07
2 changed files with 117 additions and 50 deletions
  1. 113 44
      src/components/NodeMap.vue
  2. 4 6
      src/helpers/d3Data.js

+ 113 - 44
src/components/NodeMap.vue

@@ -2,45 +2,69 @@
   <svg
     width="100%" height="100%"
     ref="svg" :id="id"
+    :view-box.camel="viewBox"
   >
-    <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]"
+    <g :id="id + '-map'" v-if="ready">
+      <g>
+        <path
+          v-for="(item, index) in links"
+          :key="`link_${index}`"
+          :d="lineGenerator([item.source, item.target])"
+          :class="['link', item.linkType || item.target.data.linkType]"
+        />
+      </g>
+
+      <circle
+        v-for="(node, index) in nodes"
+        :key="'circle' + index"
+        :cx="node.x"
+        :cy="node.y"
+        class="svg-dot-btn"
+        :class="['svg-dot-btn-' + node.data.variant, { 'origin': node.data.isOrigin }]"
+        tabindex="0"
+        @click="onNodeClick(node.data)"
+        @keyup.enter="onNodeClick(node.data)"
+        @mouseenter="activeNode = node"
       />
 
-      <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)"
+      <g>
+        <!-- rect for forcing full render -->
+        <rect
+          x="-50%" y="-50%" fill="transparent"
+          width="100%" height="100%" style="pointer-events: none;"
+        />
+        <foreignObject
+          v-if="activeNode"
+          :x="activeNode.x"
+          :y="activeNode.y"
         >
-          <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>
+          <dot-button
+            :variant="activeNode.data.variant"
+            class="active"
+            @click="onNodeClick(activeNode.data)"
+            @mouseleave="activeNode = null"
+          >
+            <template v-if="activeNode.data.variant !== 'depart'">
+              {{ activeNode.data.families[0].name }}<br>
+            </template>
+            {{ toCommaList(activeNode.data.authors) }},<br>
+            <span v-if="activeNode.data.field_titre_regular" v-html="activeNode.data.field_titre_regular + ','" />
+            <span v-html="activeNode.data.field_titre_italique || '<em>pas de titre ital</em>'" />
+          </dot-button>
+        </foreignObject>
+      </g>
+    </g>
   </svg>
 </template>
 
 <script>
 import { line } from 'd3-shape'
 import { selectAll } from 'd3-selection'
+import { select, zoom } from 'd3'
 import { drag } from 'd3-drag'
 import {
   forceSimulation, forceLink, forceManyBody,
-  forceX, forceY, forceCenter
+  forceX, forceY
 } from 'd3-force'
 
 
@@ -54,8 +78,7 @@ export default {
   props: {
     nodes: { type: Array, required: true },
     links: { type: Array, required: true },
-    id: { type: String, default: 'node-map' },
-    showId: { type: Boolean, default: true }
+    id: { type: String, default: 'node-map' }
   },
 
   data () {
@@ -64,11 +87,28 @@ export default {
       width: 100,
       height: 100,
       simulation: forceSimulation(),
-      lineGenerator: line().x(node => node.x).y(node => node.y)
+      lineGenerator: line().x(node => node.x).y(node => node.y),
+      activeNode: null
+    }
+  },
+
+  computed: {
+    viewBox () {
+      const { width, height } = this
+      return `-${width / 2} -${height / 2} ${width} ${height}`
     }
   },
 
   methods: {
+    toCommaList (arr) {
+      // FIXME TEMP some texts doesn't have authors
+      try {
+        return arr.map(({ name }) => name).join(', ')
+      } catch {
+        return 'pas d\'auteur⋅rices'
+      }
+    },
+
     updateSize () {
       const { width, height } = this.$el.getBoundingClientRect()
       Object.assign(this.$data, { width, height })
@@ -90,6 +130,14 @@ export default {
 
     initListeners () {
       // TODO replace with vue events ?
+      const svg = select('#' + this.id)
+      const map = select('#' + this.id + '-map')
+      svg.call(zoom()
+        .extent([[0, 0], [this.width, this.height]])
+        .scaleExtent([0.3, 1])
+        .on('zoom', ({ transform }) => {
+          map.attr('transform', transform)
+        }))
 
       selectAll(`#${this.id} circle`)
         .data(this.nodes)
@@ -103,11 +151,18 @@ export default {
     setupForces () {
       this.simulation
         .nodes(this.nodes)
-        .force('link', forceLink(this.links).id(d => d.id).distance(50))
-        .force('charge', forceManyBody().strength(-150))
+        .force('charge', forceManyBody().strength((d) => {
+          if (d.data.linkType === 'parents') return -2000
+          if (d.data.linkType === 'siblings') return -500
+          return -1000
+        }))
+        .force('link', forceLink(this.links).id(d => d.id).distance((d) => {
+          if (d.linkType === 'parents') return 200
+          if (d.linkType === 'siblings') return 100
+          return 0
+        }).strength(0.5))
         .force('x', forceX())
         .force('y', forceY())
-        .force('center', forceCenter(this.width * 0.5, this.height * 0.5))
     },
 
     // D3 DRAG EVENT HANDLERS
@@ -145,7 +200,7 @@ export default {
     }
   },
 
-  async mounted () {
+  mounted () {
     this.init()
   }
 }
@@ -154,6 +209,7 @@ export default {
 <style lang="scss" scoped>
 .link {
   stroke: grey;
+  vector-effect: non-scaling-stroke;
 
   &.siblings {
     stroke-dasharray: 4;
@@ -165,21 +221,34 @@ export default {
   }
 }
 
-text {
-  font-size: 0.7rem;
-  user-select: none;
-}
+.svg-dot-btn {
+  r: 9.5px;
 
-@each $id in map-keys($families){
-  .family-#{$id} {
-    fill: setColorFromId($id);
-    @if $id == 9 {
-      stroke: black;
+  @each $color, $value in $theme-colors {
+    &-#{$color} {
+      fill: $value;
+
+      @if $color == 'depart' {
+        stroke: $black;
+        stroke-width: 3px;
+        vector-effect: non-scaling-stroke;
+      }
     }
   }
+
+  &.origin {
+    r: 15px;
+  }
+}
+
+foreignObject {
+  height: 1px;
+  width: 1px;
+  overflow: visible;
 }
 
-.first {
-  stroke-width: 2px;
+.dot-btn {
+  vertical-align: top;
+  transform: translate(-50%, -50%);
 }
 </style>

+ 4 - 6
src/helpers/d3Data.js

@@ -22,7 +22,7 @@ function getLinked (text) {
 }
 
 export function toSingleManyData (rawData) {
-  rawData.first = true
+  rawData.isOrigin = true
   const h = hierarchy(rawData, d => getLinked(d))
   h.each(node => {
     if (node.parent && node.children) {
@@ -35,8 +35,7 @@ export function toSingleManyData (rawData) {
   const links = h.links()
   nodes.forEach(node => {
     Object.assign(node.data, {
-      type: node.data.type.toLowerCase(),
-      class: 'family-' + node.data.families[0].id
+      type: node.data.type.toLowerCase()
     })
   })
   return { nodes, links }
@@ -44,7 +43,7 @@ export function toSingleManyData (rawData) {
 
 
 export function toManyManyData (rawData) {
-  rawData.first = true
+  rawData.isOrigin = true
   const h = hierarchy(rawData, d => getLinked(d))
   h.each(node => {
     if (node.parent && node.children) {
@@ -79,8 +78,7 @@ export function toManyManyData (rawData) {
       id: data.id,
       data: {
         ...data,
-        type: data.type.toLowerCase(),
-        class: 'family-' + data.families[0].id
+        type: data.type.toLowerCase()
       }
     }
   })