Reputation: 950
I have multiple svg groups (each containing a circle and text) which I am dragging via d3-drag from an initial position. I have a rectangular hit zone that I only want one of these draggable groups in at a time. So whenever two groups are in the hit zone, I would like the first group that was in the hit zone to fade away and reappear in its initial position.
I have tried doing this via a function which translates the group back to its initial position by finding the current position of the circle shape and translating like:
translate(${-current_x}, ${-current_y})
This does translate the group back to the (0,0) position, so I have to offset by its initial position. I do this by setting the initial x and y values of the circle shape as attributes in the circle element and incorporating these into the translation:
translate(${-current_x + initial_x}, ${-current_y + initial_y})
Here is a block of my attempt:
https://bl.ocks.org/interwebjill/fb9b0d648df769ed72aeb2755d3ff7d5
And here it is in snippet form:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("initial_x", (d, i) => circleRadius * (2 * i + 1))
.attr("initial_y", circleRadius)
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise().classed("active", true);
}
function dragged() {
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
}
function dragEnded() {
d3.select(this).classed("active", false);
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
let current_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('cx');
let current_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('cy');
let initial_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_x');
let initial_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_y');
d3.select(`#${resetVar}`)
.transition()
.duration(2000)
.style('opacity', 0)
.transition()
.duration(2000)
.attr('transform', `translate(${-current_x}, ${-current_y})`)
.transition()
.duration(2000)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>
Here are the problems:
While using translate(${-current_x}, ${-current_y})
works, when I try using translate(${-current_x + initial_x}, ${-current_y + initial_y})
, the translation uses very large negative numbers (for example, translate(-52640, -4640)
).
While using translate(${-current_x}, ${-current_y})
works, when I try to drag this translated group again, the group immediately repeats the previous translate(${-current_x}, ${-current_y})
Upvotes: 2
Views: 149
Reputation: 38201
Your code runs into difficulties because you are positioning both the g
elements and the children text
and circle
s.
Circles and text are originally positioned by x/y attributes:
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
Drag events move the circles and text here:
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
And then we reset the circles and text by trying to offset the parent g
with a transform:
d3.select(`#${resetVar}`).attr('transform', `translate(${-current_x}, ${-current_y})`)
Where current_x
and current_y
are the current x,y values for the circles and text. We have also stored the initial x,y values for the text, but altogether, this becomes a more convoluted then necessary as we have two competing sets of positioning coordinates.
This can be simplified a fair amount. Instead of positioning both the text and the circles, simply apply a transform to the parent g
holding both the circle and the text. Then when we drag we update the transform, and when we finish, we reset the transform.
Now we have no modification of x,y/cx,cy attributes and transforms for positioning the elements relative to one another. No offsets and the parent g
's transform will always represent the position of the circle and the text.
Below I keep track of the original transform with the datum (not an element attribute) - normally I would use a property of the datum, but you have non-object data, so I just replace the datum with the original transform:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
// Immovable placemarkers:
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
// Dragables
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter()
.append("g")
.datum(function(d,i) {
return "translate("+[circleRadius * (2 * i + 1),circleRadius]+")";
})
.attr("transform", (d,i) => d)
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise();
}
function dragged() {
d3.select(this).attr("transform","translate("+[d3.event.x,d3.event.y]+")")
}
function dragEnded() {
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
d3.select(`#${resetVar}`)
.transition()
.duration(500)
.style('opacity', 0)
.transition()
.duration(500)
.attr("transform", (d,i) => d)
.transition()
.duration(500)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>
Upvotes: 3