Browse Source

feat: pages visualize improvements (#1914)

* viz pages | add ctrl-click to open page in new tab

* viz pages | rewrite `hierarchy` method

* viz pages | pan and zoom

* fix: pages visualize height + UI fixes

Co-authored-by: NGPixel <github@ngpixel.com>
Elliott Shugerman 5 years ago
parent
commit
ab1f93be1b
1 changed files with 64 additions and 38 deletions
  1. 64 38
      client/components/admin/admin-pages-visualize.vue

+ 64 - 38
client/components/admin/admin-pages-visualize.vue

@@ -29,7 +29,7 @@
             v-btn.px-5(value='rradial')
             v-btn.px-5(value='rradial')
               v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
               v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
               span.text-none Relational Radial
               span.text-none Relational Radial
-        .admin-pages-visualize-svg.pa-10(ref='svgContainer')
+        .admin-pages-visualize-svg(ref='svgContainer', v-show='pages.length >= 1')
         v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
         v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
 </template>
 </template>
 
 
@@ -61,8 +61,14 @@ export default {
   },
   },
   methods: {
   methods: {
     goToPage (d) {
     goToPage (d) {
-      if (_.get(d, 'data.id', 0) > 0) {
-        this.$router.push(`${d.data.id}`)
+      const id = d.data.id
+      if (id) {
+        if (d3.event.ctrlKey || d3.event.metaKey) {
+          const { href } = this.$router.resolve(String(id))
+          window.open(href, '_blank')
+        } else {
+          this.$router.push(String(id))
+        }
       }
       }
     },
     },
     bilink (root) {
     bilink (root) {
@@ -86,41 +92,36 @@ export default {
       }
       }
       return root
       return root
     },
     },
-    hierarchy (data, rootOnly = false) {
-      let result = []
-      let level = { result }
-      const map = new Map(data.map(d => [d.path, d]))
-      data.forEach(d => {
-        const pathParts = d.path.split('/')
-        pathParts.reduce((r, part, i) => {
-          const curPath = _.take(pathParts, i + 1).join('/')
-          if (!r[part]) {
-            r[part] = { result: [] }
-            const page = map.get(curPath)
-            r.result.push(page ? {
-              ...d,
-              children: r[part].result
-            } : {
-              title: part,
-              links: [],
-              path: curPath,
-              children: r[part].result
-            })
-          }
-
-          return r[part]
-        }, level)
-      })
+    hierarchy (pages) {
+      const map = new Map(pages.map(p => [p.path, p]))
+      const getPage = path => map.get(path) || {
+        path: path,
+        title: path.split('/').slice(-1)[0],
+        links: []
+      }
 
 
-      return rootOnly ? _.head(result) || { children: [] } : {
-        children: result
+      function recurse (depth, [parent, descendants]) {
+        const truncatePath = path => _.take(path.split('/'), depth).join('/')
+        const descendantsByChild =
+          Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
+            .map(([childPath, descendantsGroup]) => [getPage(childPath), descendantsGroup])
+            .map(([child, descendantsGroup]) =>
+              [child, _.filter(descendantsGroup, d => d.path !== child.path)])
+        return {
+          ...parent,
+          children: descendantsByChild.map(_.partial(recurse, depth + 1))
+        }
       }
       }
+      const root = { path: this.currentLocale, title: this.currentLocale, links: [] }
+      // start at depth=2 because we're taking {locale} as the root and
+      // all paths start with {locale}/
+      return recurse(2, [root, pages])
     },
     },
     /**
     /**
      * Relational Radial
      * Relational Radial
      */
      */
     drawRelations () {
     drawRelations () {
-      const data = this.hierarchy(this.pages, true)
+      const data = this.hierarchy(this.pages)
 
 
       const line = d3.lineRadial()
       const line = d3.lineRadial()
         .curve(d3.curveBundle.beta(0.85))
         .curve(d3.curveBundle.beta(0.85))
@@ -136,7 +137,13 @@ export default {
       const svg = d3.create('svg')
       const svg = d3.create('svg')
         .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
         .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
 
 
-      const link = svg.append('g')
+      const g = svg.append('g')
+
+      svg.call(d3.zoom().on('zoom', function() {
+        g.attr('transform', d3.event.transform)
+      }))
+
+      const link = g.append('g')
         .attr('stroke', '#CCC')
         .attr('stroke', '#CCC')
         .attr('fill', 'none')
         .attr('fill', 'none')
         .selectAll('path')
         .selectAll('path')
@@ -146,7 +153,7 @@ export default {
         .attr('d', ([i, o]) => line(i.path(o)))
         .attr('d', ([i, o]) => line(i.path(o)))
         .each(function(d) { d.path = this })
         .each(function(d) { d.path = this })
 
 
-      svg.append('g')
+      g.append('g')
         .attr('font-family', 'sans-serif')
         .attr('font-family', 'sans-serif')
         .attr('font-size', 10)
         .attr('font-size', 10)
         .selectAll('g')
         .selectAll('g')
@@ -195,7 +202,7 @@ export default {
      * Hierarchical Tree
      * Hierarchical Tree
      */
      */
     drawTree () {
     drawTree () {
-      const data = this.hierarchy(this.pages, true)
+      const data = this.hierarchy(this.pages)
 
 
       const treeRoot = d3.hierarchy(data)
       const treeRoot = d3.hierarchy(data)
       treeRoot.dx = 10
       treeRoot.dx = 10
@@ -212,7 +219,16 @@ export default {
       const svg = d3.create('svg')
       const svg = d3.create('svg')
         .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])
         .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])
 
 
-      const g = svg.append('g')
+      // this extra level is necessary because the element that we
+      // apply the zoom tranform to must be above the element where
+      // we apply the translation (`g`), or else zoom is wonky
+      const gZoom = svg.append('g')
+
+      svg.call(d3.zoom().on('zoom', function() {
+        gZoom.attr('transform', d3.event.transform)
+      }))
+
+      const g = gZoom.append('g')
         .attr('font-family', 'sans-serif')
         .attr('font-family', 'sans-serif')
         .attr('font-size', 10)
         .attr('font-size', 10)
         .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)
         .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)
@@ -270,7 +286,14 @@ export default {
       const svg = d3.create('svg')
       const svg = d3.create('svg')
         .style('font', '10px sans-serif')
         .style('font', '10px sans-serif')
 
 
-      svg.append('g')
+      const g = svg.append('g')
+
+      svg.call(d3.zoom().on('zoom', function () {
+        g.attr('transform', d3.event.transform)
+      }))
+
+      // eslint-disable-next-line no-unused-vars
+      const link = g.append('g')
         .attr('fill', 'none')
         .attr('fill', 'none')
         .attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
         .attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
         .attr('stroke-opacity', 0.4)
         .attr('stroke-opacity', 0.4)
@@ -282,7 +305,7 @@ export default {
           .angle(d => d.x)
           .angle(d => d.x)
           .radius(d => d.y))
           .radius(d => d.y))
 
 
-      const node = svg.append('g')
+      const node = g.append('g')
         .attr('stroke-linejoin', 'round')
         .attr('stroke-linejoin', 'round')
         .attr('stroke-width', 3)
         .attr('stroke-width', 3)
         .selectAll('g')
         .selectAll('g')
@@ -371,9 +394,12 @@ export default {
 <style lang='scss'>
 <style lang='scss'>
 .admin-pages-visualize-svg {
 .admin-pages-visualize-svg {
   text-align: center;
   text-align: center;
+  // 100vh - header - title section - footer - content padding
+  height: calc(100vh - 64px - 92px - 32px - 16px);
 
 
   > svg {
   > svg {
-    height: 100vh;
+    height: 100%;
+    width: 100%;
   }
   }
 }
 }
 </style>
 </style>