| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 | 
							- <template lang='pug'>
 
-   v-container(fluid, grid-list-lg)
 
-     v-layout(row wrap)
 
-       v-flex(xs12)
 
-         .admin-header
 
-           img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
 
-           .admin-header-title
 
-             .headline.blue--text.text--darken-2.animated.fadeInLeft Visualize Pages
 
-             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Dendrogram representation of your pages
 
-           v-spacer
 
-           v-select.mx-5.animated.fadeInDown.wait-p1s(
 
-             v-if='locales.length > 0'
 
-             v-model='currentLocale'
 
-             :items='locales'
 
-             style='flex: 0 1 120px;'
 
-             solo
 
-             dense
 
-             hide-details
 
-             item-value='code'
 
-             item-text='name'
 
-           )
 
-           v-btn-toggle.animated.fadeInDown(v-model='graphMode', color='primary', dense, rounded)
 
-             v-btn.px-5(value='htree')
 
-               v-icon(left, :color='graphMode === `htree` ? `primary` : `grey darken-3`') mdi-sitemap
 
-               span.text-none Hierarchical Tree
 
-             v-btn.px-5(value='hradial')
 
-               v-icon(left, :color='graphMode === `hradial` ? `primary` : `grey darken-3`') mdi-chart-donut-variant
 
-               span.text-none Hierarchical Radial
 
-             v-btn.px-5(value='rradial')
 
-               v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
 
-               span.text-none Relational Radial
 
-         .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!
 
- </template>
 
- <script>
 
- import _ from 'lodash'
 
- import * as d3 from 'd3'
 
- import gql from 'graphql-tag'
 
- /* global siteConfig, siteLangs */
 
- export default {
 
-   data() {
 
-     return {
 
-       graphMode: 'htree',
 
-       width: 800,
 
-       radius: 400,
 
-       pages: [],
 
-       locales: siteLangs,
 
-       currentLocale: siteConfig.lang
 
-     }
 
-   },
 
-   watch: {
 
-     pages () {
 
-       this.redraw()
 
-     },
 
-     graphMode () {
 
-       this.redraw()
 
-     }
 
-   },
 
-   methods: {
 
-     goToPage (d) {
 
-       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) {
 
-       const map = new Map(root.descendants().map(d => [d.data.path, d]))
 
-       for (const d of root.descendants()) {
 
-         d.incoming = []
 
-         d.outgoing = []
 
-         d.data.links.forEach(i => {
 
-           const relNode = map.get(i)
 
-           if (relNode) {
 
-             d.outgoing.push([d, relNode])
 
-           }
 
-         })
 
-       }
 
-       for (const d of root.descendants()) {
 
-         for (const o of d.outgoing) {
 
-           if (o[1]) {
 
-             o[1].incoming.push(o)
 
-           }
 
-         }
 
-       }
 
-       return root
 
-     },
 
-     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: []
 
-       }
 
-       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
 
-      */
 
-     drawRelations () {
 
-       const data = this.hierarchy(this.pages)
 
-       const line = d3.lineRadial()
 
-         .curve(d3.curveBundle.beta(0.85))
 
-         .radius(d => d.y)
 
-         .angle(d => d.x)
 
-       const tree = d3.cluster()
 
-         .size([2 * Math.PI, this.radius - 100])
 
-       const root = tree(this.bilink(d3.hierarchy(data)
 
-         .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))
 
-       const svg = d3.create('svg')
 
-         .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
 
-       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('fill', 'none')
 
-         .selectAll('path')
 
-         .data(root.descendants().flatMap(leaf => leaf.outgoing))
 
-         .join('path')
 
-         .style('mix-blend-mode', 'multiply')
 
-         .attr('d', ([i, o]) => line(i.path(o)))
 
-         .each(function(d) { d.path = this })
 
-       g.append('g')
 
-         .attr('font-family', 'sans-serif')
 
-         .attr('font-size', 10)
 
-         .selectAll('g')
 
-         .data(root.descendants())
 
-         .join('g')
 
-         .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
 
-         .append('text')
 
-         .attr('dy', '0.31em')
 
-         .attr('x', d => d.x < Math.PI ? 6 : -6)
 
-         .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
 
-         .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
 
-         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
 
-         .attr('cursor', 'pointer')
 
-         .text(d => d.data.title)
 
-         .each(function(d) { d.text = this })
 
-         .on('mouseover', overed)
 
-         .on('mouseout', outed)
 
-         .on('click', d => this.goToPage(d))
 
-         .call(text => text.append('title').text(d => `${d.data.path}
 
-           ${d.outgoing.length} outgoing
 
-           ${d.incoming.length} incoming`))
 
-         .clone(true).lower()
 
-         .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 
-       function overed(d) {
 
-         link.style('mix-blend-mode', null)
 
-         d3.select(this).attr('font-weight', 'bold')
 
-         d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', '#2196F3').raise()
 
-         d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', '#2196F3').attr('font-weight', 'bold')
 
-         d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', '#E91E63').raise()
 
-         d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', '#E91E63').attr('font-weight', 'bold')
 
-       }
 
-       function outed(d) {
 
-         link.style('mix-blend-mode', 'multiply')
 
-         d3.select(this).attr('font-weight', null)
 
-         d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null)
 
-         d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null)
 
-         d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null)
 
-         d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null)
 
-       }
 
-       this.$refs.svgContainer.appendChild(svg.node())
 
-     },
 
-     /**
 
-      * Hierarchical Tree
 
-      */
 
-     drawTree () {
 
-       const data = this.hierarchy(this.pages)
 
-       const treeRoot = d3.hierarchy(data)
 
-       treeRoot.dx = 10
 
-       treeRoot.dy = this.width / (treeRoot.height + 1)
 
-       const root = d3.tree().nodeSize([treeRoot.dx, treeRoot.dy])(treeRoot)
 
-       let x0 = Infinity
 
-       let x1 = -x0
 
-       root.each(d => {
 
-         if (d.x > x1) x1 = d.x
 
-         if (d.x < x0) x0 = d.x
 
-       })
 
-       const svg = d3.create('svg')
 
-         .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])
 
-       // 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-size', 10)
 
-         .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)
 
-       g.append('g')
 
-         .attr('fill', 'none')
 
-         .attr('stroke', this.$vuetify.theme.dark ? '#999' : '#555')
 
-         .attr('stroke-opacity', 0.4)
 
-         .attr('stroke-width', 1.5)
 
-         .selectAll('path')
 
-         .data(root.links())
 
-         .join('path')
 
-         .attr('d', d3.linkHorizontal()
 
-           .x(d => d.y)
 
-           .y(d => d.x))
 
-       const node = g.append('g')
 
-         .attr('stroke-linejoin', 'round')
 
-         .attr('stroke-width', 3)
 
-         .selectAll('g')
 
-         .data(root.descendants())
 
-         .join('g')
 
-         .attr('transform', d => `translate(${d.y},${d.x})`)
 
-       node.append('circle')
 
-         .attr('fill', d => d.children ? '#555' : '#999')
 
-         .attr('r', 2.5)
 
-       node.append('text')
 
-         .attr('dy', '0.31em')
 
-         .attr('x', d => d.children ? -6 : 6)
 
-         .attr('text-anchor', d => d.children ? 'end' : 'start')
 
-         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
 
-         .attr('cursor', 'pointer')
 
-         .text(d => d.data.title)
 
-         .on('click', d => this.goToPage(d))
 
-         .clone(true).lower()
 
-         .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 
-       this.$refs.svgContainer.appendChild(svg.node())
 
-     },
 
-     /**
 
-      * Hierarchical Radial
 
-      */
 
-     drawRadialTree () {
 
-       const data = this.hierarchy(this.pages)
 
-       const tree = d3.tree()
 
-         .size([2 * Math.PI, this.radius])
 
-         .separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)
 
-       const root = tree(d3.hierarchy(data)
 
-         .sort((a, b) => d3.ascending(a.data.title, b.data.title)))
 
-       const svg = d3.create('svg')
 
-         .style('font', '10px sans-serif')
 
-       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('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
 
-         .attr('stroke-opacity', 0.4)
 
-         .attr('stroke-width', 1.5)
 
-         .selectAll('path')
 
-         .data(root.links())
 
-         .join('path')
 
-         .attr('d', d3.linkRadial()
 
-           .angle(d => d.x)
 
-           .radius(d => d.y))
 
-       const node = g.append('g')
 
-         .attr('stroke-linejoin', 'round')
 
-         .attr('stroke-width', 3)
 
-         .selectAll('g')
 
-         .data(root.descendants().reverse())
 
-         .join('g')
 
-         .attr('transform', d => `
 
-           rotate(${d.x * 180 / Math.PI - 90})
 
-           translate(${d.y},0)
 
-         `)
 
-       node.append('circle')
 
-         .attr('fill', d => d.children ? '#555' : '#999')
 
-         .attr('r', 2.5)
 
-       node.append('text')
 
-         .attr('dy', '0.31em')
 
-         /* eslint-disable no-mixed-operators */
 
-         .attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)
 
-         .attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')
 
-         .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
 
-         /* eslint-enable no-mixed-operators */
 
-         .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
 
-         .attr('cursor', 'pointer')
 
-         .text(d => d.data.title)
 
-         .on('click', d => this.goToPage(d))
 
-         .clone(true).lower()
 
-         .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
 
-       this.$refs.svgContainer.appendChild(svg.node())
 
-       function autoBox() {
 
-         const {x, y, width, height} = this.getBBox()
 
-         return [x, y, width, height]
 
-       }
 
-       svg.attr('viewBox', autoBox)
 
-     },
 
-     redraw () {
 
-       while (this.$refs.svgContainer.firstChild) {
 
-         this.$refs.svgContainer.firstChild.remove()
 
-       }
 
-       if (this.pages.length > 0) {
 
-         switch (this.graphMode) {
 
-           case 'rradial':
 
-             this.drawRelations()
 
-             break
 
-           case 'htree':
 
-             this.drawTree()
 
-             break
 
-           case 'hradial':
 
-             this.drawRadialTree()
 
-             break
 
-         }
 
-       }
 
-     }
 
-   },
 
-   apollo: {
 
-     pages: {
 
-       query: gql`
 
-         query ($locale: String!) {
 
-           pages {
 
-             links(locale: $locale) {
 
-               id
 
-               path
 
-               title
 
-               links
 
-             }
 
-           }
 
-         }
 
-       `,
 
-       variables () {
 
-         return {
 
-           locale: this.currentLocale
 
-         }
 
-       },
 
-       fetchPolicy: 'network-only',
 
-       update: (data) => data.pages.links,
 
-       watchLoading (isLoading) {
 
-         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
 
-       }
 
-     }
 
-   }
 
- }
 
- </script>
 
- <style lang='scss'>
 
- .admin-pages-visualize-svg {
 
-   text-align: center;
 
-   // 100vh - header - title section - footer - content padding
 
-   height: calc(100vh - 64px - 92px - 32px - 16px);
 
-   > svg {
 
-     height: 100%;
 
-     width: 100%;
 
-   }
 
- }
 
- </style>
 
 
  |