소스 검색

update LibraryTree with MapZoom

axolotle 3 년 전
부모
커밋
cfee58bde7
4개의 변경된 파일148개의 추가작업 그리고 16개의 파일을 삭제
  1. 1 1
      src/components/nodes/NodeViewHeaderRef.vue
  2. 143 12
      src/pages/library/LibraryTree.vue
  3. 2 3
      src/store/modules/library.js
  4. 2 0
      src/store/utils.js

+ 1 - 1
src/components/nodes/NodeViewHeaderRef.vue

@@ -7,7 +7,7 @@
         <strong class="d-block">
           {{ toCommaList(node.authors) }},
         </strong>
-        <span v-if="node.preTitle" v-html="node.preTitle + ','" />
+        <span v-if="node.preTitle" v-html="trim(node.preTitle) + ','" />
         <em v-html="(trim(node.title) || 'pas de titre ital') + ','" />
         <div v-if="node.edition" class="edition">
           {{ node.edition.map(ed => ed.name).join(' ') }}

+ 143 - 12
src/pages/library/LibraryTree.vue

@@ -1,35 +1,112 @@
 <template>
-  <b-overlay :show="nodeTree === undefined" class="h-100">
-    <node-map
-      v-if="nodeTree !== undefined"
-      v-bind="nodeTree"
-      v-on="$listeners"
-    />
+  <b-overlay :show="nodeTree.nodes.length === 0" class="h-100">
+    <map-zoom id="library-tree" :min-zoom="0.3" :max-zoom="1">
+      <path
+        v-for="link in nodeTree.links"
+        :key="`${link.source.data.id}_${link.target.data.id}`"
+        :d="lineGenerator([link.source, link.target])"
+        :class="['svg-link', (link.linkType || link.target.data.linkType)]"
+      />
+
+      <circle
+        v-for="node in nodeTree.nodes"
+        :key="'circle' + node.data.id"
+        :cx="node.x"
+        :cy="node.y"
+        class="svg-dot"
+        :class="['svg-dot-' + node.data.variant, { 'origin': node.parent === null }]"
+        tabindex="0"
+        @click="onNodeClick(node.data)"
+        @keyup.enter="onNodeClick(node.data)"
+        @mouseenter="activeNode = node"
+      />
+
+      <g>
+        <rect class="svg-overlay" />
+        <foreignObject
+          v-if="activeNode"
+          :x="activeNode.x"
+          :y="activeNode.y"
+        >
+          <dot-button
+            :variant="activeNode.data.variant"
+            class="active"
+            @click="onNodeClick(activeNode.data)"
+            @mouseleave="activeNode = null"
+          >
+            <template v-if="activeNode.data.variant !== 'depart'">
+              {{ $t('variants.' + activeNode.data.variant) }}<br>
+            </template>
+            {{ toCommaList(activeNode.data.authors) }},<br>
+            <span v-if="activeNode.data.preTitle" v-html="trim(activeNode.data.preTitle) + ','" />
+            <span v-html="trim(activeNode.data.title) || '<em>pas de titre ital</em>'" />
+          </dot-button>
+        </foreignObject>
+      </g>
+    </map-zoom>
   </b-overlay>
 </template>
 
 <script>
 import { mapGetters } from 'vuex'
+import { line } from 'd3-shape'
+import { forceSimulation, forceLink, forceManyBody, forceX, forceY } from 'd3-force'
 
-import NodeMap from '@/components/NodeMap'
+import { trim, toCommaList } from '@/helpers/common'
+import { MapZoom } from '@/components/layouts'
 
 
 export default {
-  name: 'TreeMap',
+  name: 'LibraryTree',
 
   components: {
-    NodeMap
+    MapZoom
   },
 
   data () {
     return {
-      loading: true,
-      mapData: undefined
+      activeNode: null
     }
   },
 
   computed: {
-    ...mapGetters(['nodeTree'])
+    ...mapGetters(['nodeTree']),
+
+    // ONE TIME GETTER
+    simulation () {
+      return forceSimulation()
+        .force('charge', forceManyBody().strength((d) => {
+          if (d.data.linkType === 'parents') return -2000
+          if (d.data.linkType === 'siblings') return -500
+          return -1000
+        }))
+        .force('link', forceLink().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())
+    },
+
+    // ONE TIME GETTER
+    lineGenerator () {
+      return line().x(node => node.x).y(node => node.y)
+    }
+  },
+
+  watch: {
+    nodeTree (tree) {
+      this.activeNode = null
+      this.simulation.nodes(tree.nodes)
+      this.simulation.force('link').links(tree.links)
+      this.simulation.alpha(0.5).restart()
+    }
+  },
+
+  methods: {
+    toCommaList,
+    trim
   },
 
   created () {
@@ -39,4 +116,58 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+.svg {
+  &-link {
+    stroke: grey;
+    vector-effect: non-scaling-stroke;
+
+    &.siblings {
+      stroke-dasharray: 4;
+    }
+
+    &.parents {
+      stroke: red;
+      opacity: .3;
+    }
+  }
+
+  &-dot {
+    r: 9.5px;
+
+    @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;
+    }
+  }
+
+  &-overlay {
+    width: 100%;
+    height: 100%;
+    x: -50%;
+    y: -50%;
+    fill: transparent;
+    pointer-events: none;
+  }
+}
+
+foreignObject {
+  height: 1px;
+  width: 1px;
+  overflow: visible;
+}
+
+.dot-btn {
+  transform: translate(-50%, -50%);
+}
 </style>

+ 2 - 3
src/store/modules/library.js

@@ -177,7 +177,7 @@ export default {
       })
     },
 
-    activeNodes: (state) => {
+    activeNodes: state => {
       if (!state.nodebook) return []
       return state.nodebook.reduce((acc, ids) => [...acc, ...ids], [])
     },
@@ -218,8 +218,7 @@ export default {
     },
 
     nodeTree: (state, rootState) => {
-      if (state.nodeDepartId === undefined) return
-      return state.trees[state.nodeDepartId]
+      return state.trees[state.nodeDepartId] || { nodes: [], links: [] }
     }
   }
 }

+ 2 - 0
src/store/utils.js

@@ -92,6 +92,8 @@ export function buildTree (treeData, nodesData) {
     if (!uniqueIds.includes(node.id)) {
       uniqueIds.push(node.id)
       node.data = nodesData.find(n => n.id === node.id)
+      // Add `x` and `y` keys so Vue can update on these values updates
+      Object.assign(node, { x: undefined, y: undefined })
       nodes.push(node)
       if (node.children) {
         node.children.forEach(child => {