I am creating a floor map using d3.js and Vue js. I want to add a feature where the user clicks a button which creates a rectangle (a doorway) that the user can change the scale and X,Y positions. I am quite new to vue js and d3.js so I am using this as a reference. The button works and adds X amount of rectangles for X amount of presses. However, when I add the first doorway and try to drag the rectangle it is completely off from the user's mouse coordinates. Further, when I add the second, third, fourth, etc rectangle I can't change the position of those, only the first rectangle. I think this is because I'm only selecting one rectangle in the dragging function. However Im more concerned about getting the dragging right on the rectangles. Would someone be able to point me in the right direction please?
<template>
<div id = "app">
<header>
<Navbar />
</header>
<div class = "floor">
<div id = "tooltip"></div>
<svg width = "850" height = "800">
</svg>
</div>
<div class = "buttons">
<button v-on:click="addRec()"> Add Doorway </button>
</div>
</div>
</template>
addRec(){
console.log("Doorway Added")
var doorGroup = d3.selectAll("svg")
.append("g")
//setting attributes for doorway
var doorway = doorGroup.append("rect")
.attr("width",100)
.attr("height", 25)
.attr("x", 150)
.attr("y", 200)
//setting style attributes for doorway
.attr("fill","#F5F5F5")
.attr("stroke", "black")
.attr("stroke-width", 2.5)
.attr("cursor", "move")
//binding drag method onto rectangle
.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd)
)
},
//this is to be called once used has clicked on the doorway
dragStart(d,i,nodes){
d3.select(nodes[i])
.style("stroke", "red")
},
/*
this is to be called once used has started dragging the doorway
dragging(d) upadtes the position of the doorway, from the mouse X,Y Coors
*/
dragging(d,i,nodes){
var width = 850
var height = 800
var xCoor = d3.pointer(event)[0]
var yCoor = d3.pointer(event)[1]
// d3.select(nodes[i])
// .attr("transform", "translate(" + (xCoor - 200) + "," + (yCoor - 300) + ")")
d3.select(nodes[i])
.attr("x", xCoor)
.attr("y", yCoor)
},
//this is to be called once user has clicked off the doorway
dragEnd(d,i,nodes){
d3.select(nodes[i])
.style("stroke", "black")
},
There are a two issues in your code:
All your drag functions use:
d3.select("rect")
This will select the first matching element in the DOM. This will always be the same rectangle, it doesn't correspond to the rectangle being dragged. Instead, you can access the proper rectangle with:
function dragFunction() {
d3.select(this);
}
In d3v5 and earlier, if this
isn't available you can use:
dragFunction(d,i,nodes) => d3.select(nodes[i])
There isn't a great way to get your rectangle if you can't access this
yet in d3v6 - the migration guide shows a "old" way to get this
that involves filtering elements for a datum match. I've never seen that approach used in d3v5 or earlier, d3.select(nodes[i])
was always the fall back if this
wasn't available. So, ideally, you can access this
- don't use arrow functions for the listeners
When initially appending the rectangles you position by x,y. When dragging you apply a translate, but don't remove the initial x,y positioning. This results in the x,y coordinates being applied twice, once with their original values, and once with their dragged values relative to the origin (not their starting position). You should modify the same property for initial placement and for drag - either x,y attributes or the transform, but don't use both for placement.
D3v5
Along with using d3.event.x and d3.event.y, (d3.pointer was added in d3v6), I've made the above changes and believe I've created the intended effect below.
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.select("button")
.on("click", function() {
svg.append("rect")
.attr("width",10)
.attr("height", 25)
.attr("x", 150)
.attr("y", 100)
.attr("fill","#1b1c3b")
.attr("opacity", ".5")
.attr("stroke", "black")
.attr("stroke-width", "2")
.attr("cursor", "move")
.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd)
)
function dragStart(d,i,nodes){
d3.select(nodes[i])
.style("stroke", "red")
}
function dragging(d,i,nodes){
var xCoor = d3.event.x;
var yCoor = d3.event.y;
d3.select(nodes[i])
.attr("x", xCoor)
.attr("y", yCoor);
}
function dragEnd(d,i,nodes){
d3.select(nodes[i])
.style("stroke", "black")
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Add Rect</button>
D3v6
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.select("button")
.on("click", function() {
svg.append("rect")
.attr("width",10)
.attr("height", 25)
.attr("x", 150)
.attr("y", 100)
.attr("fill","#1b1c3b")
.attr("opacity", ".5")
.attr("stroke", "black")
.attr("stroke-width", "2")
.attr("cursor", "move")
.call(d3.drag()
.on('start', dragStart)
.on('drag', dragging)
.on('end', dragEnd)
)
function dragStart(event,d){
d3.select(this)
.style("stroke", "")
}
function dragging(event,d){
var xCoor = event.x;
var yCoor = event.y;
d3.select(this)
.attr("x", xCoor)
.attr("y", yCoor);
}
function dragEnd(event,d){
d3.select(this)
.style("stroke", "black")
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.1.0/d3.min.js"></script>
<button>Add Rect</button>