|
@@ -1,255 +0,0 @@
|
|
|
-<template>
|
|
|
- <svg
|
|
|
- width="100%" height="100%"
|
|
|
- ref="svg" :id="id"
|
|
|
- :view-box.camel="viewBox"
|
|
|
- >
|
|
|
- <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.parent === null }]"
|
|
|
- tabindex="0"
|
|
|
- @click="onNodeClick(node.data)"
|
|
|
- @keyup.enter="onNodeClick(node.data)"
|
|
|
- @mouseenter="activeNode = node"
|
|
|
- />
|
|
|
-
|
|
|
- <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"
|
|
|
- >
|
|
|
- <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="activeNode.data.preTitle + ','" />
|
|
|
- <span v-html="activeNode.data.title || '<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
|
|
|
-} 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' }
|
|
|
- },
|
|
|
-
|
|
|
- data () {
|
|
|
- return {
|
|
|
- ready: false,
|
|
|
- width: 100,
|
|
|
- height: 100,
|
|
|
- simulation: forceSimulation(),
|
|
|
- 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 })
|
|
|
- },
|
|
|
-
|
|
|
- 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 ?
|
|
|
- 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)
|
|
|
- .call(drag()
|
|
|
- .on('start', this.onNodeDragStart)
|
|
|
- .on('drag', this.onNodeDrag)
|
|
|
- .on('end', this.onNodeDragEnd)
|
|
|
- )
|
|
|
- },
|
|
|
-
|
|
|
- setupForces () {
|
|
|
- this.simulation
|
|
|
- .nodes(this.nodes)
|
|
|
- .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())
|
|
|
- },
|
|
|
-
|
|
|
- // 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) {
|
|
|
- // FIXME CURRENT PROBLEM AS API TREATING TEXTPRODs as TEXTREFs (no `parents:`)
|
|
|
- const { id, parents } = nodeData
|
|
|
- if (parents) {
|
|
|
- this.$emit('open-node', parents[0].id, id)
|
|
|
- } else {
|
|
|
- this.$emit('open-node', id)
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- mounted () {
|
|
|
- this.init()
|
|
|
- }
|
|
|
-}
|
|
|
-</script>
|
|
|
-
|
|
|
-<style lang="scss" scoped>
|
|
|
-.link {
|
|
|
- stroke: grey;
|
|
|
- vector-effect: non-scaling-stroke;
|
|
|
-
|
|
|
- &.siblings {
|
|
|
- stroke-dasharray: 4;
|
|
|
- }
|
|
|
-
|
|
|
- &.parents {
|
|
|
- stroke: red;
|
|
|
- opacity: .3;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.svg-dot-btn {
|
|
|
- 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;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-foreignObject {
|
|
|
- height: 1px;
|
|
|
- width: 1px;
|
|
|
- overflow: visible;
|
|
|
-}
|
|
|
-
|
|
|
-.dot-btn {
|
|
|
- vertical-align: top;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
-}
|
|
|
-</style>
|