update NodeMap to display DotButton and add zoom/pan

This commit is contained in:
axolotle
2021-05-23 21:47:40 +02:00
parent f014a0b654
commit 3d28a54f07
2 changed files with 117 additions and 50 deletions
+113 -44
View File
@@ -2,45 +2,69 @@
<svg
width="100%" height="100%"
ref="svg" :id="id"
:view-box.camel="viewBox"
>
<template v-if="ready">
<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 :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.data.isOrigin }]"
tabindex="0"
@click="onNodeClick(node.data)"
@keyup.enter="onNodeClick(node.data)"
@mouseenter="activeNode = node"
/>
<template v-for="(node, index) in nodes">
<circle
:key="'circle' + index"
:cx="node.x"
:cy="node.y"
:class="node.data.class + (node.data.first ? ' first' : '')"
:r="node.children ? 7 : 5"
tabindex="0"
@click="onNodeClick(node.data)"
@keyup.enter="onNodeClick(node.data)"
<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"
>
<title>{{ node.data.title }}</title>
</circle>
<text
v-if="showId"
:key="'text' + index" :x="node.x + 7" :y="node.y-7"
>{{ node.data.id }}</text>
</template>
</template>
<dot-button
:variant="activeNode.data.variant"
class="active"
@click="onNodeClick(activeNode.data)"
@mouseleave="activeNode = null"
>
<template v-if="activeNode.data.variant !== 'depart'">
{{ activeNode.data.families[0].name }}<br>
</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>
</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, forceCenter
forceX, forceY
} from 'd3-force'
@@ -54,8 +78,7 @@ export default {
props: {
nodes: { type: Array, required: true },
links: { type: Array, required: true },
id: { type: String, default: 'node-map' },
showId: { type: Boolean, default: true }
id: { type: String, default: 'node-map' }
},
data () {
@@ -64,11 +87,28 @@ export default {
width: 100,
height: 100,
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: {
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 })
@@ -90,6 +130,14 @@ export default {
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)
@@ -103,11 +151,18 @@ export default {
setupForces () {
this.simulation
.nodes(this.nodes)
.force('link', forceLink(this.links).id(d => d.id).distance(50))
.force('charge', forceManyBody().strength(-150))
.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())
.force('center', forceCenter(this.width * 0.5, this.height * 0.5))
},
// D3 DRAG EVENT HANDLERS
@@ -145,7 +200,7 @@ export default {
}
},
async mounted () {
mounted () {
this.init()
}
}
@@ -154,6 +209,7 @@ export default {
<style lang="scss" scoped>
.link {
stroke: grey;
vector-effect: non-scaling-stroke;
&.siblings {
stroke-dasharray: 4;
@@ -165,21 +221,34 @@ export default {
}
}
text {
font-size: 0.7rem;
user-select: none;
}
.svg-dot-btn {
r: 9.5px;
@each $id in map-keys($families){
.family-#{$id} {
fill: setColorFromId($id);
@if $id == 9 {
stroke: black;
@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;
}
}
.first {
stroke-width: 2px;
foreignObject {
height: 1px;
width: 1px;
overflow: visible;
}
.dot-btn {
vertical-align: top;
transform: translate(-50%, -50%);
}
</style>
+4 -6
View File
@@ -22,7 +22,7 @@ function getLinked (text) {
}
export function toSingleManyData (rawData) {
rawData.first = true
rawData.isOrigin = true
const h = hierarchy(rawData, d => getLinked(d))
h.each(node => {
if (node.parent && node.children) {
@@ -35,8 +35,7 @@ export function toSingleManyData (rawData) {
const links = h.links()
nodes.forEach(node => {
Object.assign(node.data, {
type: node.data.type.toLowerCase(),
class: 'family-' + node.data.families[0].id
type: node.data.type.toLowerCase()
})
})
return { nodes, links }
@@ -44,7 +43,7 @@ export function toSingleManyData (rawData) {
export function toManyManyData (rawData) {
rawData.first = true
rawData.isOrigin = true
const h = hierarchy(rawData, d => getLinked(d))
h.each(node => {
if (node.parent && node.children) {
@@ -79,8 +78,7 @@ export function toManyManyData (rawData) {
id: data.id,
data: {
...data,
type: data.type.toLowerCase(),
class: 'family-' + data.families[0].id
type: data.type.toLowerCase()
}
}
})