Here is a solution to a similar visualization requirement, however the functional requirement was slightly different.
Here, nodes are grouped into different "zones", as in your case. The user is then required to drag nodes from one zone to another, but some zones, based on node and zone characteristics, are disabled or "dead".
It uses a standard force
that.simulation = this.d3.forceSimulation()
.force('collide',d3.forceCollide()
.radius(d => {
return d.type === 'count' ? 60 : 30
}))
// .force('charge',d3.forceManyBody()
// .strength(10))
.on('tick',ticked)
The code below is the dragged
handler, which restricts nodes from dead zones.
It is NOT fully debugged, and hasn't been touched since October, but hopefully it will help.
Below the dragged handler is the ticked
function, which positions elements initially, and after dragging.
dragged
The key part is about halfway down, after these comments:
// positioning of dragged node under cursor
// respecting all deadzone and perimiter boundaries and node radius
// drag in progress handler
// d = the d3 object associated to the dragged circle
function dragged(d) {
that.trace()
let debugcoord = [10,20]
let r = RAD_KEY + 4
let dx = d3.event.dx
let dy = d3.event.dy
let fx = d.fx
let fy = d.fy
let nx = fx+dx
let ny = fy+dy
// DEBUGGING CODE BELOW, DO NOT DELETE
// let curX = Math.round(d3.event.sourceEvent.clientX-that.g.node().getClientRects()[0].left)
// let curY = Math.round(d3.event.sourceEvent.clientY-that.g.node().getClientRects()[0].top)
// that.g.selectAll('text.coordinates').remove()
// that.g
// .append('text')
// .classed('coordinates',true)
// .attr('x',debugcoord[0])
// .attr('y',debugcoord[1])
// .text(`${curX},${curY}`)
// check out this fx,fy description for reference:
// https://stackoverflow.com/a/51548821/4256677
// deadzones are the inverse of livezones
let deadzone = []
// the names of all valid transitions, used to calculate livezones
let trans = !!d.trans ? d.trans.map(t => t.to.name) : []
// the name value of the workflow status of the dragged node
let name = d.name
// all the zones:
let zones = d3.selectAll('g.zone.group > rect.zone')
// the maximum x+width value of all nodes in 'zones' array
let right = d3.max(zones.data(), n => parseFloat(n.x)+parseFloat(n.width))
// the maximum y+height value of all nodes in 'zones' array
let bottom = d3.max(zones.data(), n => parseFloat(n.y)+parseFloat(n.height))
// the zones which represent valid future states to transition
// the dragged node (issue)
let livezones = zones.filter(function(z,i,nodes) {
// the current zone object
let zone = d3.select(this)
// the name of the current zone object
let zonename = zone.attr('data-zone')
// boolean referring to the current zone representing a valid future
// state for the node
let isLive = trans.includes(zonename) || name == zonename
// deadzone recognition and caching
if(!isLive)
{
let coords = {name:zonename,
x1:parseFloat(z.x),
x2:parseFloat(z.x)+parseFloat(z.width),
y1:parseFloat(z.y),
y2:parseFloat(z.y)+parseFloat(z.height)}
deadzone.push(coords)
}
return isLive
}).classed('live',true) // css for livezones
d3.selectAll('rect.zone:not(.live)').classed('dead',true) // css for deadzones
// positioning of dragged node under cursor
// respecting all deadzone and perimiter boundaries and node radius
that.nodes.filter(function(d) { return d.dragging; })
.each(function(d) {
if(deadzone.length > 0)
{
d.fx += deadzone.reduce((a,c) => {
a =
// node is in graph
(nx > 0 + r && nx < right - r)
// deadzone is in left column and node is to the right or above or below
&& ((c.x1 == 0 && (nx > c.x2 + r || ny < c.y1 - r || ny > c.y2 + r))
// or deadzone is in the right column and node is to the left, above or below
|| (c.x2 == right && (nx < c.x1 - r || ny < c.y1 - r || ny > c.y2 + r))
// or deadzone is not in left column and node is to the left, right, above or below
|| (c.x1 > 0 && (nx < c.x1 - r || nx > c.x2 + r || ny < c.y1 - r || ny > c.y2 + r))
)
? dx : 0
return a
},0)
d.fy += deadzone.reduce((a,c) => {
a =
// node is in graph
(ny > 0 + r && ny < bottom - r)
// deadzone is in top row and node is below or to the left or right
&& ((c.y1 == 0 && (ny > c.y2 + r || nx < c.x1 - r || nx > c.x2 + r))
// or deadzone is in the right column and node is to the left, above or below
|| (c.y2 == bottom && (ny < c.y1 - r || nx < c.x1 - r || nx > c.x2 + r))
// or deadzone is not in top row and node is above, below, left or right
|| (c.y1 > 0 && (ny < c.y1 - r || ny > c.y2 + r || nx < c.x1 - r || nx > c.x2 + r))
)
? dy : 0
// DEBUGGING CODE BELOW, DO NOT DELETE
// that.g
// .append('text')
// .classed('coordinates',true)
// .attr('x',debugcoord[0])
// .attr('y',debugcoord[1]+25)
// .text(`${Math.round(nx)},${Math.round(ny)} vs ${r},${r},${right-r},${bottom-r}`)
// that.g
// .append('text')
// .classed('coordinates',true)
// .attr('x',10)
// .attr('x',debugcoord[0])
// .attr('y',debugcoord[1]+50)
// .text(`dz coords: ${c.x1},${c.y1} ${c.x2},${c.y2}`)
// that.g
// .append('text')
// .classed('coordinates',true)
// .attr('x',debugcoord[0])
// .attr('y',debugcoord[1]+75)
// .text(c.name)
return a
},0)
}
else
{
d.fx += dx
d.fy += dy
}
})
}
ticked
The key part is after the comment // if were no longer dragging
function ticked(e) {
if(!!that.links && that.links.length > 0)
{
that.links
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
}
that.nodes
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.each(function(d) {
if(typeof d.selected === 'undefined')
d.selected = false
if(typeof d.previouslySelected === 'undefined')
d.previouslySelected = false
})
that.labels
.attr("x", function(d) { return d.x })
.attr("y", function(d) {
return d.type === 'count' ? d.y+6 : d.y+4
})
.attr('class',(d) => { return that.getClassFromNodeName(d.name)})
.classed('count', (d) => {
return d.type === 'count' ? true : false
})
// if were no longer dragging
if(!that.dragging)
{
let k = 4*this.alpha()
that.nodes.each(function(n,i) {
let zclass = that.getClassFromNodeName(n.name)
let z = that.zones[zclass]
n.x += (z.x + z.width/2 - n.x) * k
n.y += (z.y + z.height/2 - n.y) * k
})
}
that.nodes
// .each(pos)
.attr('cx',d => { return d.x }) //boundary(d,'x')})
.attr('cy',d => { return d.y }) //boundary(d,'y')})
that.labels
// .each(pos)
.attr('x',d => { return d.x }) //boundary(d,'x')})
.attr('y',d => { return d.y + (d.type === 'count'?6:4) }) //boundary(d,'y') + (d.type === 'count'?6:4)})
}