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 <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>
+4 -6
View File
@@ -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
} }
} }
}) })