D3 Modify Path's y - javascript

I have some user events that append different paths to an svg. What I would like is for each subsequent path to be drawn below the previous path (+ some padding). I would assume this would be done by style.top or .attr('y', my_value). However neither worked for me. And if nothing is done to address this, then the paths will be drawn on top of eachother, which is bad. Should be straight forward, but for added clarity, let me provide the crucial code:
graphGroup.append("path")
.datum(data)
.attr("fill", "none")
.attr("d", line);
I suppose I could programtically create n number of gs, (i.e. graphGroup2 = svg.append('g').attr('transform', 'translate(' + my_transformation +')') and so forth, but I think there should be an easier way. Maybe a dynamic y:
//var count = graphGroup....???
//count could be a way to count the existing number of paths in the g graphGroup
graphGroup.append("path")
.datum(data)
.attr("fill", "none")
.attr('y', count*200)
.attr("d", line);
Obviously, I don't know how to make my dynamic approach work. If you think it could work, you can build off it, or feel free to scrap it in favor of another way.

A SVG <path> element has no y attribute. You'll have to use translate to set the vertical position of those paths. You don't have to create additional < g> elements, though: all the paths can be in the same group.
Here is a demo, where I'm increasing a count variable inside a loop to translate the paths down:
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var d = "M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80";
var count = 0;
(function loop() {
if (count++ > 5) return;
setTimeout(function() {
svg.append("path")
.attr("stroke", "black")
.attr("d", d)
.attr("fill", "none")
.attr("transform", "translate(0," + count * 20 + ")")
loop();
}, 1000);
})();
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

Append two elements in svg at the same level

I'm using d3.js
Hi, I'm having an issue finding how to append two elements (path and image) to the same g (inside my svg) from the same data. I know how to do this, but the tricky thing is I need to get the BBox values of the "path" elements in order to place the "image" elements in the middle... My goal is actually to place little clouds in the center of cities on a map like this : this is the map I am trying to reproduce
On the map it's not centered but I have to do so. So this is my current code:
// Draw the map
svg.append("g")
.selectAll("path")
.data(mapEPCI.features)
.enter()
.append("path")
.attr("fill", d => d.properties.color)
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")
.append("image")
.attr("xlink:href", function(d) {
if (d.properties.plan_air == 1)
return ("data/page8_territoires/cloud.png")
else if (d.properties.plan_air == 2)
return ("data/page8_territoires/cloudgray.png")
})
.attr("width", "20")
.attr("height", "15")
.attr("x", function (d) {
let bbox = d3.select(this.parentNode).node().getBBox();
return bbox.x + 30})
.attr("y", function (d) {
return d3.select(this.parentNode).node().getBBox().y + 30})
This gets the right coordinates for my images but it's because the parent node is actually the path... If I append the image to the g element, is there a way to get the "BrotherNode", or maybe the last child of the "g" element ? I don't know if I'm clear enough but I hope you get my point.
I'm kinda new to js so maybe I'm missing something simple I just don't know yet
Thanks for your help
I would handle your data at the g level and create a group for every map feature (country) which contains the path and a sibling image:
<!doctype html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<svg width="600" height="600"></svg>
<script>
let svg = d3.select('svg'),
mapEPCI = {
features: [100, 200, 300, 400]
};
let g = svg.selectAll('g')
.data(mapEPCI.features)
// enter selection is collection of g
let ge = g.enter().append("g");
// append a path to each g according to data
ge.append('path')
.attr("d", (d) => "M" + d + ",10L" + d + ",100")
.style("stroke", "black");
// append a sibling image
ge.append("image")
.attr("xlink:href", "https://placeimg.com/20/15/animals")
.attr("width", "20")
.attr("height", "15")
.attr("transform", function(d) {
// find my sibling path to get bbox
let sibling = this.parentNode.firstChild;
let bbox = sibling.getBBox();
return "translate(" + (bbox.x - 20 / 2) + "," + (bbox.y + bbox.height / 2 - 15 / 2) + ")"
});
</script>
</body>
</html>

how to obtain a complete clockwise rotation in D3

I want a square to complete a full clockwise rotation on itself, after a pause on the half of the rotation.
The following code makes it doing an half rotation clockwise, and the other half counter-clockwise, contrary to what I expect.
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s
.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attr("transform", "translate(100,100) rotate(360)");
* {
margin: 0;
padding: 0;
border: 0;
}
body {
background: #ffd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I can hack such a code splitting the second half rotation in two quarter clockwise rotations, but I wish to know if there is a more elegant solution.
The culprit here is D3 itself, not any SVG spec.
The problem is that your transition uses d3.interpolateTransform, as we can see here:
var fullname = namespace(name), i = fullname === "transform" ? interpolateTransform : interpolate;
This is v4 source code, not v3, but the principle is the same, as you can see in the actual v3 code:
var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);
Then, if we look in the source code for interpolateTransform (again, v4, but v3 is almost the same), we'll see that it uses a function called parseSvg that calculates the matrix for the new transform:
function parseSvg(value) {
if (value == null) return identity;
if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgNode.setAttribute("transform", value);
if (!(value = svgNode.transform.baseVal.consolidate())) return identity;
value = value.matrix;
return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
}
That function is generating 0 as the final value in the matrix when you pass rotate(360) to it (the actual value is -2.4492935982947064e-16, which is practically zero).
Solution
There are several possible solutions here, the easiest one is using interpolateString instead of interpolateTransform.
Also, since your code uses D3 v3, you can take advantage of d3.transform(), which was removed in v4/v5:
d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
Here is your code with that change:
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attrTween("transform", function() {
return d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
});
<svg></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
If I use D3v3 it does not rotate to 180 degrees, if I switch to D3v4 it rotates to 180.
You can interpolate to 359.99. It does not follow the string interpolator from the docs, because you also get scale() in the transform.
translate(100, 100) rotate(359.989990234375) scale(0.999,0.999)
This does not happen if you write your own interpolator.
var svg = d3.select('svg');
var s=svg.append("rect")
.attr("width",50)
.attr("height",50)
.attr("x",-25)
.attr("y",-25)
.attr("fill","red")
.attr("transform","translate(100,100) rotate(0)");
s
.transition()
.duration(3000)
.ease(d3.easeLinear)
.attr("transform","translate(100,100) rotate(180)")
.transition()
.delay(2000)
.duration(3000)
.ease(d3.easeLinear)
.attrTween("transform", () => d3.interpolate("translate(100,100) rotate(180)", "translate(100,100) rotate(360)") );
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

How to do stroke-opacity:0 with d3

Here is my code. My target - do intervals between slices in pie chart.
chart.svg.selectAll('path')
.style('stroke-opacity','0.0')
.style('stroke-width','10');
I think if stroke opacity will be 0 on piechart slices on web page it will be similar to interval between slices.
Problem: if stroke opacity equals to zero that doesn't work. If equals to number from 0.1 to 1.0 - all works. But I have another color from background.
Please give a hand to beginner! Thanks for attention and have a nice day.
I believe the problem comes from the misconception that, when you set stroke-opacity to 0, the stroke will be transparent and reveal the background colour, and the fill of the element will end at the internal limits of the stroke. But, in fact, if you set the stroke-opacity to 0, you'll reveal the fill of the element (and the background colour, once the stroke goes inwards and outwards in the default stroke-alignment).
Look, for instance, at this example:
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var color = d3.scale.category10();
data = [10, 20];
var rects = svg.selectAll(".rect")
.data(data)
.enter()
.append("rect");
rects.attr("x", function(d){ return d*10})
.attr("y", 0)
.attr("width", 100)
.attr("height", 80)
.attr("fill", function(d){ return color(d)})
.attr("stroke-width", 10)
.attr("stroke", "white")
.attr("stroke-opacity", 0);
var rects2 = svg.selectAll(".rect2")
.data(data)
.enter()
.append("rect");
rects2.attr("x", function(d){ return d*10})
.attr("y", 100)
.attr("width", 100)
.attr("height", 80)
.attr("fill", function(d){ return color(d)})
.attr("stroke-width", 10)
.attr("stroke", "white");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
Both the pairs of rectangles are absolutely equal:
rects.attr("x", function(d){ return d*10})
.attr("y", 0)
.attr("width", 100)
.attr("height", 80)
.attr("fill", function(d){ return color(d)})
.attr("stroke-width", 10)
.attr("stroke", "white")
The only difference is that, in the upper pair, I add:
.attr("stroke-opacity", 0);
And that is the same of having no stroke.
You can see that, independent of the stroke alignment, the area and the size of the element is the same. Check the default stroke:
The rect element, outlined by a black line, remains the same.
To finish, I just found this fiddle (I don't know who's the author), and I set the stroke to white and stroke-width to 10: this is what you want, imitating a real padding. But you'll not get this result setting the stroke opacity to 0: https://jsfiddle.net/j1769sx2/

How to style SVG path to take an image?

Here is my JSFiddle.
I am simply trying to set up this image in the middle of my arc. My best intuition tells to use .attr("fill","url('somePicture')"), but for the life of me that hasn't been a viable solution.
var width = 700,
height = 600,
tau = 2 * Math.PI
var arc = d3.svg.arc()
.innerRadius(100)
.outerRadius(250)
.startAngle(0)
var arc2 = d3.svg.arc()
.innerRadius(0)
.outerRadius(100)
.startAngle(0)
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")")
//gray null background
var background = svg.append("path")
.datum({endAngle: tau})
.style("fill", "#ddd")
.attr("d", arc)
var center = svg.append("image")
.append("path")
.datum({endAngle: tau})
.attr("d", arc2)
.attr("class","record")
.attr("xlink:href", "http://lorempixel.com/g/400/400/")
You don't need to define a path. If you look into your html, the image is there but it's of size 0x0.
var center = svg.append("image")
.datum({endAngle: tau})
.attr("d", arc2)
.attr("class","record")
.attr("width",400)
.attr("height",400)
.attr("x",-200)
.attr("y",-200)
.attr("xlink:href", "http://cdn.mysitemyway.com/etc-mysitemyway/icons/legacy-previews/icons/glossy-black-icons-symbols-shapes/018712-glossy-black-icon-symbols-shapes-shapes-circle.png")
In your fiddle you attached the wrong image. If you keep your code the same aside from this it should work. Good luck.
If I understand you correctly, you mean that if you want to resize the whole thing, then the image will change with it. Correct? You can do that by making all numbers functions of others.
I start with defining
innerR = 100
for lack of a better name. Then all other non-zero functions are functions of that. There is a fiddle here. Experiment with that parameter and see what happens.

Using arc.Centroid to put an svg:image in the middle of the arc - but not appearing

I am currently trying to place a svg:image in the centre of my arc:
var arcs = svg.selectAll("path");
arcs.append("svg:image")
.attr("xlink:href", "http://www.e-pint.com/epint.jpg ")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("width", "150px")
.attr("height", "200px");
I would appreciate it if someone could give me any advice on why it isn't appearing
thanks : http://jsfiddle.net/xwZjN/17/
Looking at the jsfiddle, you are creating the path elements after you try to append the svg:image elements to the them. It should be the other way around. You should first create the arcs and then append the images.
Second, as far as I know, the svg:path element should not contain any svg:image tags. It doesn't seem to display them if you place some inside. Instead what you should do is create svg:g tags with class arc and then use those to place the svg:images
Slightly modifying your jsfiddle could look something like this:
var colours = ['#909090','#A8A8A8','#B8B8B8','#D0D0D0','#E8E8E8'];
var arcs = svg.selectAll("path");
for (var z=0; z<30; z++){
arcs.data(donut(data1))
.enter()
//append the groups
.append("svg:g")
.attr("class", "arc")
.append("svg:path")
.attr("fill", function(d, i) { return colours[(Math.floor(z/6))]; })
.attr("d", arc[z])
.attr("stroke","black")
}
//here we append images into arc groups
var pics = svg.selectAll(".arc").append("svg:image")
.attr("xlink:href", "http://www.e-pint.com/epint.jpg ")
.attr("transform", function(d,i) {
//since you have an array of arc generators I used i to find the arc
return "translate(" + arc[i].centroid(d) + ")"; })
.attr("x",-5)
.attr("y",-10)
.attr("width", "10px")
.attr("height", "20px");
Where I also decreased the size of the images and offset them so that they fit into the arc.

Categories