Move a circle 100px forward on every click event - javascript

I am learning D3.js. I wrote following code which moves the circle on click. I want to move this circle forward (+100px) from its current position on every click.
I want to do it without CSS
Following is my code
var svgContainer = d3.select("body").append("svg").attr("id", "root")
.attr("width", 500)
.attr("height", 500).append("g")
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("id", "Zubi")
.attr("cx", 100)
.attr("cy", 30)
.attr("r", 20)
.on("click", function() {
d3.select(this)
.transition()
.duration(3000)
.style("fill", "red")
.attr("cx", 100)
})
//.attr("transform", "translate(200,200)")})

The easiest way to achieve what you want is using a getter (selection.attr("cx")) to get the current position and increasing it. You can also use a translate, but since you're a D3 learner using the circle's cx attribute is probably easier to understand.
Here is a demo, each click increases 50px:
const circle = d3.select("circle")
.on("click", function() {
d3.select(this)
.transition()
.attr("cx", +d3.select(this).attr("cx") + 50)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100">
<circle cx="50" cy="50" r="30" fill="teal"></circle>
</svg>
Have in mind that the getter always returns a string, so you have to coerce it to a number.
Also, since you have just one element in the selection and it has a name, you don't need d3.select(this):
const circle = d3.select("circle")
.on("click", function() {
circle.transition()
.attr("cx", +circle.attr("cx") + 50)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100">
<circle cx="50" cy="50" r="30" fill="teal"></circle>
</svg>

Related

Clip path is moving with group of elements when using d3.drag

I'm trying to drag a group of shapes on a clipped path. For the first time, It works fine, but as soon as I started dragging, clipping does not work at all.
Here is my working code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
You can see on dragging, first time clipped shape is moving all the way. No further clipping is there.
To make it easy, I redraw the outer circle again. Check this code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
// redraw circle to make it easy
svg.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
Here You can see clipping is not working at all. I want to bound this dragging within circle and if moves out of clipping boundaries, it should clip it accordingly.
Can anyone help me out with this requirement? Or let me know where I'm
doing it wrong.
The drag callback is transforming the same g element that the clip path has been applied to. This means that the g element's clip path is also being transformed, which is why the clipped shape is moving around as you drag your shape.
The snippet below uses a grey rectangle to show the clip path definition, and a pink rectangle to show the area of the transformed g element. The circle is retaining the original clip shape because the g element's clip path is being translated along with the rest of the element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)" transform="translate(50, 50)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</svg>
In the snippet below, a clip path is applied to an outer g element (which is not translated and has the same co-ordinates as the original clip path definition), while the transformation is applied to an inner g element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<g transform="translate(100, 50)">
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</g>
</svg>
So, as shown in the example you should apply the clip path to an outer g element, while transforming an inner g element.

Function filter not working as expected in d3js

I have this HTML structure
<g class="type type-project" id="g-nsmart_city_lab" transform="translate(954.9537424482861,460.65694411587845)">
<circle class="highlighter-circles" fill-opacity="0" r="70" fill="rgb(150,150,150)" id="hc-nsmart_city_lab"></circle>
<circle class="node" r="50" fill="#768b83" id="nsmart_city_lab" filter="url(#blur)"></circle>
<text font-family="Comic Sans MS" font-size="18px" fill="black" class="nodetext" id="t-nsmart_city_lab" style="text-anchor: middle;" x="0" y="0">SMART CITY LAB</text>
<image href="./icons/project.svg" width="30" height="30" id="i-nsmart_city_lab" class="nodeimg"></image>
<image href="./icons/expand2.svg" width="30" height="30" for-node="nsmart_city_lab" x="25" y="-45" id="ne-nsmart_city_lab" class="nodeexp" style="visibility: hidden;" data-expandable="false"></image>
<circle class="inv_node" r="50" fill="red" fill-opacity="0" id="inv_nsmart_city_lab"></circle>
</g>
and I want to to something with the g elements that fulfill certain condition. But when doing,
d3.selectAll("g.type").filter(g_element => g_element.class !== "whatever");
the filter does not work as expected (at least for me). g_element.class is undefined. After debugging, for some reason the filtering is returning <circle class="node" r="50" fill="#768b83" id="nsmart_city_lab" filter="url(#blur)"></circle> instead of a g object to access its attributes and do the filtering.
How could this be done then ?
Here you have a jsfiddle which always returns undefined, https://jsfiddle.net/k6Ldxtof/40/
In your snippet...
d3.selectAll("g.type").filter(g_element => g_element.class !== "whatever");
... the first argument, which you named g_element, is the datum bound to that element. As there is no data bound here, that's obviously undefined.
To get the element instead, you have to use this. However, since you're using a arrow function here, you need to use the second and third arguments combined:
d3.selectAll("g.type")
.filter((_,i,n) => console.log(n[i]))
Then to get the classes, you can simply use a getter...
d3.selectAll("g.type")
.filter((_,i,n) => console.log(d3.select(n[i]).attr("class")))
Or, even simpler, using classList:
d3.selectAll("g.type")
.filter((_, i, n) => console.log(n[i].classList))
Here is the demo:
function create() {
let g = d3.select("body")
.append("svg")
.attr("height", "500")
.attr("width", "500")
.append("g");
g.append("g")
.attr("class", "type type-red")
.attr("data-color", "red")
.append("circle")
.attr("r", 50)
.attr("fill", "red")
.attr("cx", 50)
.attr("cy", 50);
g.append("g")
.attr("class", "type type-green")
.attr("data-color", "green")
.append("circle")
.attr("r", 50)
.attr("fill", "green")
.attr("cx", 200)
.attr("cy", 50);
g.append("g")
.attr("class", "type type-blue")
.attr("data-color", "blue")
.append("circle")
.attr("r", 50)
.attr("fill", "blue")
.attr("cx", 100)
.attr("cy", 150);
filter_out();
}
/***************** USING THE SELECTOR ********************/
function filter_out() {
d3.selectAll("g.type")
.filter((_, i, n) => console.log(n[i].classList))
.attr("opacity", 0.5);
}
create();
<script src="https://d3js.org/d3.v4.js"></script>

Conditional transition not behaving as expected

I want to transition the green circle and then the red circle. Transitioning the green circle works but when I transition the red circle, the green one reverts back to its original position. How can I avoid this?
https://jsfiddle.net/jtr13/cwckLv29/1/
<svg width="300" height="300">
<circle cx="50" cy="100" r="20" fill="green">
</circle>
<circle cx="100" cy="150" r="20" fill="red">
</circle>
</svg>
<script>
d3.selectAll("circle")
.transition().duration(1000)
.attr("cx", function() {
if (d3.select(this)
.style('fill') === "rgb(0, 128, 0)") { // green circle gets new cx
return 150;
} else {
return d3.select(this).attr("cx") // red circle keeps cx
} } );
d3.selectAll("circle")
.transition().duration(1000).delay(1000)
.attr("cx", function() {
if (d3.select(this)
.style('fill') === "rgb(255, 0, 0)") { // red circle gets new cx
return 200;
} else {
return d3.select(this).attr("cx") // green circle reverts to
// original cx, not changed cx... why???
} } );
</script>
There is an important but subtle distinction between attr and attrTween in a transition. The reason why you are having this problem stems from this distinction, while there may be easier ways of resolving this problem without worrying about the mechanics of it, my answer will look at the why of your problem and provides a solution in relation to that.
First, with some modification to your transitions, we can see the problem more clearly:
var svg = d3.select("svg");
d3.selectAll("circle")
.transition().duration(1000)
.on("start",function() { console.log("start transition1"); })
.attr("cx",150);
d3.selectAll("circle")
.transition()
.delay(1000)
.on("start",function() { return console.log("start transition2"); })
.attr("cx", function() {
console.log("transition2 cx value: ", d3.select(this).attr("cx"));
return d3.select(this).attr("cx")
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="300" height="300">
<circle cx="50" cy="100" r="20" fill="green">
</circle>
</svg>
The value used for the end point of the transition 2 is determined prior to transition 2 starting, in fact it is determined prior to transition 1 starting! As the value used in transition.attr() is determined prior to the transition start, at transition initialization, the value isn't updated after the first transition. This is also true if using .transition().transition() (as it not functionally different than the approach you have used).
From the life of a transition documentation for d3:
Methods that specify target values (such as transition.attr) are
evaluated synchronously; however, methods that require the starting
value for interpolation, such as transition.attrTween and
transition.styleTween, must be deferred until the transition starts. (link).
Consequently, one solution is to use attrTween. This will allow for the cx value being calculated on transition start, rather than initialization (I've used a fill on transition for the following snippets to show the second transition is working, as cx is not modified):
var svg = d3.select("svg");
d3.selectAll("circle")
.transition().duration(1000)
.on("start",function() { console.log("start transition1"); })
.attr("cx",150)
d3.selectAll("circle")
.transition()
.delay(1000)
.duration(1000)
.on("start",function() { return console.log("start transition2"); })
.attr("fill","steelblue")
.attrTween("cx", function() {
var x = d3.select(this).attr("cx");
console.log("transition2 cx value: ", x);
return function() { return x };
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="300" height="300">
<circle cx="50" cy="100" r="20" fill="green">
</circle>
</svg>
Based on the console, you can see a value for cx that is current as of the beginning of the transition.
Alternatively, you could opt not to initialize a transition until you get to the end of the previous one. Using the end event of the transition, we can create a new transition that is initialized with the current value of cx:
var svg = d3.select("svg");
d3.selectAll("circle")
.transition().duration(1000)
.on("start",function() { console.log("start transition1"); })
.attr("cx",150)
.on("end", function() {
d3.select(this).transition()
.duration(1000)
.on("start",function() { return console.log("start transition2"); })
.attr("fill","steelblue")
.attr("cx", function() {
var x = d3.select(this).attr("cx");
console.log("transition2 cx value: ", x);
return x;
});
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="300" height="300">
<circle cx="50" cy="100" r="20" fill="green">
</circle>
</svg>
What you're trying to do is not the idiomatic D3 and it will fail for the reasons already explained by Andrew in his answer. Just do what everybody does: .on("end", etc....
However, just for the sake of curiosity and completeness, what you're trying to do is (almost) possible! Just bind data in the "first" transition and use the bound datum:
d3.selectAll("circle")
.transition().duration(1000)
.attr("cx", function(d) {
if (d3.select(this)
.style('fill') === "rgb(0, 128, 0)") {
return d.x = 150;
} else {
return d.x = d3.select(this).attr("cx")
}
});
d3.selectAll("circle")
.transition().duration(1000).delay(1000)
.attr("cx", function(d) {
if (d3.select(this)
.style('fill') === "rgb(255, 0, 0)") {
return 200;
} else {
return d.x
}
});
Here is the demo:
d3.selectAll("circle").each(function() {
d3.select(this).datum({
x: 0
})
});
d3.selectAll("circle")
.transition().duration(1000)
.attr("cx", function(d) {
if (d3.select(this)
.style('fill') === "rgb(0, 128, 0)") {
return d.x = 150;
} else {
return d.x = d3.select(this).attr("cx")
}
});
d3.selectAll("circle")
.transition().duration(1000).delay(1000)
.attr("cx", function(d) {
if (d3.select(this)
.style('fill') === "rgb(255, 0, 0)") {
return 200;
} else {
return d.x
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="300" height="300">
<circle cx="50" cy="100" r="20" fill="green">
</circle>
<circle cx="100" cy="150" r="20" fill="red">
</circle>
</svg>

d3.js: grouping with <g> (with the data join enter/update/exit cycle)

I made a JSFiddle to explain my problem visually: https://jsfiddle.net/nph/poyxrmn7/
I am trying to use the <g> element effectively in my d3 code to move groups of objects in a way that works with transitions.
As an example, I am trying to make a graph with labeled points. I can do it this way, and it works:
var labeledCircles1 = function(data){
var svg = d3.select('svg');
var circles = svg.selectAll('circle')
.data(data);
circles.enter().append('circle')
circles.exit().remove()
circles
.attr('cx', function(d,i){ return i; })
.attr('cy', function(d){ return d; })
.attr('r', 10)
var texts = svg.selectAll('text')
.data(data)
texts.enter().append('text')
texts.exit().remove()
texts
.text(function(d){ return d; })
.attr('x', function(d,i){ return i; })
.attr('y', function(d){ return d; })
.attr('dy', -10)
};
It outputs something like this:
<svg>
<circle cx="0" cy="10" r="10"></circle>
<circle cx="1" cy="30" r="10"></circle>
<text x="0" y="10" dy="-10">10</text>
<text x="1" y="30" dy="-10">30</text>
</svg>
But this involves repeating myself a lot, and if I were to add any more elements to each point, I would have to repeat myself again. It also doesn't group the elements logically within the generated svg, as shown above.
I would like to be able to specify the positions only once, and have the groups be more logical. I want code that outputs this:
<svg>
<g transform="translate(0,10)">
<circle r="10"></circle>
<circle r="10"></circle>
</g>
<g transform="translate(1,30)">
<text dy="-10">10</text>
<text dy="-10">30</text>
</g>
</svg>
But I can't figure out how to do it. How to start is clear enough:
var svg = d3.select('svg');
var groups = svg.selectAll('.group')
.data(data)
groups.enter().append('g')
.attr('class', 'group')
groups.exit().remove();
groups
.attr('transform', function(d,i){
var x = i * 20 + 50;
var y = d + 20;
return 'translate(' + x + ',' + y + ')';
})
But then I'm not sure how to proceed from there.
If I append to groups, it works the first time, but when I change the data set it redraws my circle and text elements:
groups.append('circle')
.attr('r', 10)
groups.append('text')
.text(function(d) { return d; })
.attr('dy', -10);
But if I do a selectAll on groups then (of course) it never draws the elements at all.
I'm sure there is something simple I'm missing, but I'm not sure what.
(It seems fairly likely that this is a duplicate, but I was unable to find anyone else addressing this general question.)
If you want to add data one time, the easy way is
<g>
<text></text>
<circle></circle>
</g>
<g>
<text></text>
<circle></circle>
</g>
<g>
<text></text>
<circle></circle>
</g>
<g>
<text></text>
<circle></circle>
</g>
<g>
<text></text>
<circle></circle>
</g>
And js function
var labeledCircles3 = function(data) {
var svg = d3.select('#svg3');
var groups = svg.selectAll('g').data(data)
// exit
groups.exit().remove()
// new
var newGroups = groups.enter()
var newgroup = newGroups.append('g')
newgroup.append('text')
newgroup.append('circle')
// update + new
groups = newGroups.merge(groups)
groups.select('text')
.text(function(d){ return d; })
.attr('x', function(d,i){ return i * 20 + 50; })
.attr('y', function(d){ return d + 20; })
.attr('dy', -10)
groups.select('circle')
.attr('cx', function(d,i){ return i * 20 + 50; })
.attr('cy', function(d){ return d + 20; })
.attr('r', 10)
}
https://jsfiddle.net/guanzo/poyxrmn7/1/
You need to continue performing data joins inside the nested selections. That is, a data join for the circles, and for the text. You were appending a new circle each time, which is why it was being redrawn.
var circles = groups.selectAll('circle')
.data(d=>[d])
circles.enter().append('circle')
circles.attr('r', 10)
.attr('cx', 0)
.attr('cy', 0)

D3 Angular confusion: fill color doesn't change

Develop angular application with D3 JS and faced with the problem that I can not change fill color of svg.
If you look this code, you can see that I create one svg and trying to insert another one from my vendor:
function link (scope, element, attrs) {
var svgContainer = d3.select(element[0]).append("svg")
.attr("width", $window.screenX - 2*100)
.attr("id", "oil-game")
.attr("height", 1200);
var well = svgContainer.append("g");
angular.forEach(scope.d3WellDetails, function (value, key) {
var circle = well.append("circle")
.attr("cx", 55)
.attr("cy", 100 + key*115)
.attr("r", 40)
.attr('stroke', '#0897c7')
.attr('stroke-width', '5')
.attr('fill', 'white');
well.append("text")
.attr('x', 50)
.attr('y', 85 + key*115)
.attr('fill', '#0897c7')
.attr('font-size', 16)
.attr('font-weight', 900)
.text(value.Name);
well.append('svg:image')
.attr('xlink:href', '../../images/wells.svg')
.attr('x', 40)
.attr('y', 95 + key*115)
.attr("width", 30)
.attr("height", 30)
.attr('fill', '#0897c7');
});
}
I want you to look to the final part when I'm appending new svg. If I'm using .attr('xlink:href', '//') I can't change fill color of svg.
But if I use .attr('src', '//') I don't see svg image but at developer tool I can see it as empty.
How can I solve it?
The problem in this case doesn't have to do with Angular, but rather with the way you're appending the external SVG as an image.
Instead I would suggest you append the external SVG as an SVG Definition and include it with the 'use' attribute. This will allow you to style the imported SVG after the fact. Keep in mind that inline styles in the definition will override styles added with js or css after the fact.
Here's an example: https://jsfiddle.net/3fx1553f/
HTML:
<svg id="svg-defs" version="1.1" style="display:none">
<defs>
<g id="gdef1">
<circle id="s1" cx="200" cy="200" r="100" stroke="black" stroke-width="3" />
</g>
</defs>
</svg>
<div id="container">
</div>
CSS:
.fill-red {
fill: red;
}
JS:
var svg = d3.select('#container')
.append('svg')
.attr('id', 'main')
.attr('width', '540px')
.attr('height', '540px');
svg.append('g')
.attr('class','fill-red')
.append('use')
.attr('xlink:href', '#gdef1');

Categories

Resources