update NodeMap to display DotButton and add zoom/pan
This commit is contained in:
+113
-44
@@ -2,45 +2,69 @@
|
|||||||
<svg
|
<svg
|
||||||
width="100%" height="100%"
|
width="100%" height="100%"
|
||||||
ref="svg" :id="id"
|
ref="svg" :id="id"
|
||||||
|
:view-box.camel="viewBox"
|
||||||
>
|
>
|
||||||
<template v-if="ready">
|
<g :id="id + '-map'" v-if="ready">
|
||||||
<path
|
<g>
|
||||||
v-for="(item, index) in links"
|
<path
|
||||||
:key="`link_${index}`"
|
v-for="(item, index) in links"
|
||||||
:d="lineGenerator([item.source, item.target])"
|
:key="`link_${index}`"
|
||||||
:class="['link', item.linkType || item.target.data.linkType]"
|
: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">
|
<g>
|
||||||
<circle
|
<!-- rect for forcing full render -->
|
||||||
:key="'circle' + index"
|
<rect
|
||||||
:cx="node.x"
|
x="-50%" y="-50%" fill="transparent"
|
||||||
:cy="node.y"
|
width="100%" height="100%" style="pointer-events: none;"
|
||||||
:class="node.data.class + (node.data.first ? ' first' : '')"
|
/>
|
||||||
:r="node.children ? 7 : 5"
|
<foreignObject
|
||||||
tabindex="0"
|
v-if="activeNode"
|
||||||
@click="onNodeClick(node.data)"
|
:x="activeNode.x"
|
||||||
@keyup.enter="onNodeClick(node.data)"
|
:y="activeNode.y"
|
||||||
>
|
>
|
||||||
<title>{{ node.data.title }}</title>
|
<dot-button
|
||||||
</circle>
|
:variant="activeNode.data.variant"
|
||||||
|
class="active"
|
||||||
<text
|
@click="onNodeClick(activeNode.data)"
|
||||||
v-if="showId"
|
@mouseleave="activeNode = null"
|
||||||
:key="'text' + index" :x="node.x + 7" :y="node.y-7"
|
>
|
||||||
>{{ node.data.id }}</text>
|
<template v-if="activeNode.data.variant !== 'depart'">
|
||||||
</template>
|
{{ activeNode.data.families[0].name }}<br>
|
||||||
</template>
|
</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>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { line } from 'd3-shape'
|
import { line } from 'd3-shape'
|
||||||
import { selectAll } from 'd3-selection'
|
import { selectAll } from 'd3-selection'
|
||||||
|
import { select, zoom } from 'd3'
|
||||||
import { drag } from 'd3-drag'
|
import { drag } from 'd3-drag'
|
||||||
import {
|
import {
|
||||||
forceSimulation, forceLink, forceManyBody,
|
forceSimulation, forceLink, forceManyBody,
|
||||||
forceX, forceY, forceCenter
|
forceX, forceY
|
||||||
} from 'd3-force'
|
} from 'd3-force'
|
||||||
|
|
||||||
|
|
||||||
@@ -54,8 +78,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
nodes: { type: Array, required: true },
|
nodes: { type: Array, required: true },
|
||||||
links: { type: Array, required: true },
|
links: { type: Array, required: true },
|
||||||
id: { type: String, default: 'node-map' },
|
id: { type: String, default: 'node-map' }
|
||||||
showId: { type: Boolean, default: true }
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
@@ -64,11 +87,28 @@ export default {
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
simulation: forceSimulation(),
|
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: {
|
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 () {
|
updateSize () {
|
||||||
const { width, height } = this.$el.getBoundingClientRect()
|
const { width, height } = this.$el.getBoundingClientRect()
|
||||||
Object.assign(this.$data, { width, height })
|
Object.assign(this.$data, { width, height })
|
||||||
@@ -90,6 +130,14 @@ export default {
|
|||||||
|
|
||||||
initListeners () {
|
initListeners () {
|
||||||
// TODO replace with vue events ?
|
// 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`)
|
selectAll(`#${this.id} circle`)
|
||||||
.data(this.nodes)
|
.data(this.nodes)
|
||||||
@@ -103,11 +151,18 @@ export default {
|
|||||||
setupForces () {
|
setupForces () {
|
||||||
this.simulation
|
this.simulation
|
||||||
.nodes(this.nodes)
|
.nodes(this.nodes)
|
||||||
.force('link', forceLink(this.links).id(d => d.id).distance(50))
|
.force('charge', forceManyBody().strength((d) => {
|
||||||
.force('charge', forceManyBody().strength(-150))
|
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('x', forceX())
|
||||||
.force('y', forceY())
|
.force('y', forceY())
|
||||||
.force('center', forceCenter(this.width * 0.5, this.height * 0.5))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// D3 DRAG EVENT HANDLERS
|
// D3 DRAG EVENT HANDLERS
|
||||||
@@ -145,7 +200,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted () {
|
mounted () {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +209,7 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.link {
|
.link {
|
||||||
stroke: grey;
|
stroke: grey;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
|
||||||
&.siblings {
|
&.siblings {
|
||||||
stroke-dasharray: 4;
|
stroke-dasharray: 4;
|
||||||
@@ -165,21 +221,34 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
.svg-dot-btn {
|
||||||
font-size: 0.7rem;
|
r: 9.5px;
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@each $id in map-keys($families){
|
@each $color, $value in $theme-colors {
|
||||||
.family-#{$id} {
|
&-#{$color} {
|
||||||
fill: setColorFromId($id);
|
fill: $value;
|
||||||
@if $id == 9 {
|
|
||||||
stroke: black;
|
@if $color == 'depart' {
|
||||||
|
stroke: $black;
|
||||||
|
stroke-width: 3px;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.origin {
|
||||||
|
r: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.first {
|
foreignObject {
|
||||||
stroke-width: 2px;
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-btn {
|
||||||
|
vertical-align: top;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Vendored
+4
-6
@@ -22,7 +22,7 @@ function getLinked (text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toSingleManyData (rawData) {
|
export function toSingleManyData (rawData) {
|
||||||
rawData.first = true
|
rawData.isOrigin = true
|
||||||
const h = hierarchy(rawData, d => getLinked(d))
|
const h = hierarchy(rawData, d => getLinked(d))
|
||||||
h.each(node => {
|
h.each(node => {
|
||||||
if (node.parent && node.children) {
|
if (node.parent && node.children) {
|
||||||
@@ -35,8 +35,7 @@ export function toSingleManyData (rawData) {
|
|||||||
const links = h.links()
|
const links = h.links()
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
Object.assign(node.data, {
|
Object.assign(node.data, {
|
||||||
type: node.data.type.toLowerCase(),
|
type: node.data.type.toLowerCase()
|
||||||
class: 'family-' + node.data.families[0].id
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return { nodes, links }
|
return { nodes, links }
|
||||||
@@ -44,7 +43,7 @@ export function toSingleManyData (rawData) {
|
|||||||
|
|
||||||
|
|
||||||
export function toManyManyData (rawData) {
|
export function toManyManyData (rawData) {
|
||||||
rawData.first = true
|
rawData.isOrigin = true
|
||||||
const h = hierarchy(rawData, d => getLinked(d))
|
const h = hierarchy(rawData, d => getLinked(d))
|
||||||
h.each(node => {
|
h.each(node => {
|
||||||
if (node.parent && node.children) {
|
if (node.parent && node.children) {
|
||||||
@@ -79,8 +78,7 @@ export function toManyManyData (rawData) {
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
type: data.type.toLowerCase(),
|
type: data.type.toLowerCase()
|
||||||
class: 'family-' + data.families[0].id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user