How to draw SVG circles along a curved line in d3 js?
I am coding a software in which I draw several svg circles next to each other, which the user can adjust the position by changing the coordinates of the line. The desired output is
Right now, the circles are in the wrong space, causing the circles to move to the left.
code
function curve(val){
var w = d3.select("#new_row_1").attr("width");
var
numLines = 1,
lineSpacing = 18,
parabDepth = -30;
var row_spacing = 18;
if(val == 0){
parabDepth = -18;
}
else if(val == 1){
parabDepth = 0;
}
else if(val == 2){
parabDepth = 20;
}
else if(val == 3){
parabDepth = 30;
}
else if(val == 4){
parabDepth = 40;
}
else if(val == 5){
parabDepth = 50;
// row_spacing = 18.41;
}
else if(val == 6){
// row_spacing = 18.5;
parabDepth = 60;
}
else if(val == 7){
// row_spacing = 18.6;
parabDepth = 70;
}
else if(val == 8){
// row_spacing = 18.62;
parabDepth = 80;
}
else if(val == 9){
parabDepth = 90;
}
else if(val == 10){
parabDepth = 100;
}
else if(val == 11){
parabDepth = 110;
}
else if(val == 12){
parabDepth = 120;
}
else if(val == 13){
parabDepth = 130;
}
else if(val == 14){
parabDepth = 140;
}
else if(val == 15){
parabDepth = 150;
row_spacing = 20;
}
width = w ;
var curveData = [];
curveData.push([0,0]);
curveData.push([width/3 * 1, lineSpacing + parabDepth ]);
curveData.push([width/3 * 2, lineSpacing + parabDepth ]);
curveData.push([width/3 * 3, 1]);
var line = d3.line()
.x(function(d) {
return d[0];
})
.y(function(d) {
return d[1] + 8;
})
.curve(d3.curveCardinal);
var svg = d3.select("#new_row_1").attr("height", (numLines * lineSpacing) + lineSpacing + parabDepth + row_spacing).attr("width", width);
var g = svg.selectAll(".line")
.data(d3.range(numLines))
.enter()
.append("g")
.attr("class", "line")
.attr("transform", function(d){
return "translate(7," + (d*lineSpacing) + ")";
});
var path = g.append("path")
.attr("d", line(curveData))
.style("fill", "none")
.style("stroke", "pink")
.style("stroke-width","4")
.each(function(){
var g = d3.select(this.parentNode),
self = d3.select(this),
pathLength = width;
g.selectAll("circle")
.data(d3.range(1, width, row_spacing))
.enter()
.append("circle")
.attr("transform", (d,i) => {
var p = this.getPointAtLength(d);
return "translate(" + p.x + "," + p.y + ")";
})
.attr("r", 7)
.style("fill", function(d,i){
if(i == quant_col2 - 1){
return "red";
}else if(i == quant_col2/2){
return "yellow";
}
else if(i == quant_col2 - 2){
return "green";
}
else{
return "white";
}
})
.attr("stroke","black")
.attr("stroke-width","1");
});
}
setTimeout(function(){
curve(1)
},2000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="252" height="18" id="new_row_1" class="new_row" style="top: 150px; left: 302px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><g><circle cx="8" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="3" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">1</text></g><g><circle cx="26" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="21" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">2</text></g><g><circle cx="44" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="39" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">3</text></g><g><circle cx="62" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="57" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">4</text></g><g><circle cx="80" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="75" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">5</text></g><g><circle cx="98" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="93" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">6</text></g><g><circle cx="116" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="111" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">7</text></g><g><circle cx="134" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="129" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">8</text></g><g><circle cx="152" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="147" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">9</text></g><g><circle cx="170" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="165" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">10</text></g><g><circle cx="188" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="183" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">11</text></g><g><circle cx="206" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="201" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">12</text></g><g><circle cx="224" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="219" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">13</text></g><g><circle cx="242" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="237" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">14</text><g class="label" style="display: none;"><rect x="126" y="1" width="14" height="14" style="fill: black;"></rect><text dx="130" dy="11" style="font-size: 9px; font-weight: 400; fill: white;">A</text></g></g></svg>
The value will be controlled by the user, that is, it will change dynamically. I want to distribute them evenly along the line. How can I do that?
source to share
You use getPointAtLength()
to find the X, Y coordinates of a point at a distance along the curve. Surely wrong. You want to find the Y coordinate for a given X.
There is no built-in SVG function for this. You will have to calculate this yourself. To do this, you need a polynomial form of the Bezier equation, which is not trivial.
Since you are only doing a parabola, you are better off avoiding Bézier curves and just calculate your curve with the usual simple quadratic equation (ax ^ 2 + bx + c).
Also: if you decide to stick with Bézier curves, then you should really use Quadratic Bézier and not cbic. Quadratic beziers have the property that they always form a section of a parabola.
source to share
Instead of using, width
you should use getTotalLength()
for string size:
pathLength = this.getTotalLength();
Here's a demo with this change:
function curve(val) {
var w = d3.select("#new_row_1").attr("width");
var
numLines = 1,
lineSpacing = 18,
parabDepth = -30;
var row_spacing = 18;
if (val == 0) {
parabDepth = -18;
} else if (val == 1) {
parabDepth = 0;
} else if (val == 15) {
parabDepth = 150;
row_spacing = 20;
} else {
parabDepth = val * 10;
}
width = w;
var curveData = [];
curveData.push([0, 0]);
curveData.push([width / 3 * 1, lineSpacing + parabDepth]);
curveData.push([width / 3 * 2, lineSpacing + parabDepth]);
curveData.push([width / 3 * 3 - 8, - 8]);
var line = d3.line()
.x(function(d) {
return d[0];
})
.y(function(d) {
return d[1];
})
.curve(d3.curveCardinal);
var svg = d3.select("#new_row_1").attr("height", (numLines * lineSpacing) + lineSpacing + parabDepth + row_spacing).attr("width", width);
var g = svg.selectAll(".line")
.data(d3.range(numLines))
.enter()
.append("g")
.attr("class", "line")
.attr("transform", function(d) {
return "translate(7," + (d * lineSpacing + 7) + ")";
});
var path = g.append("path")
.attr("d", line(curveData))
.style("fill", "none")
.style("stroke", "pink")
.style("stroke-width", "4")
.each(function() {
var g = d3.select(this.parentNode),
self = d3.select(this),
pathLength = this.getTotalLength();
g.selectAll("circle")
.data(d3.range(1, pathLength, row_spacing))
.enter()
.append("circle")
.attr("transform", (d, i) => {
var p = this.getPointAtLength(d);
return "translate(" + p.x + "," + p.y + ")";
})
.attr("r", 7)
.style("fill", "darkslategray")
.attr("stroke", "black")
.attr("stroke-width", "1");
});
}
setTimeout(function() {
curve(5)
}, 500)
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="252" height="18" id="new_row_1" class="new_row" style="top: 150px; left: 302px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><g><circle cx="8" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="3" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">1</text></g><g><circle cx="26" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="21" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">2</text></g><g><circle cx="44" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="39" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">3</text></g><g><circle cx="62" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="57" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">4</text></g><g><circle cx="80" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="75" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">5</text></g><g><circle cx="98" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="93" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">6</text></g><g><circle cx="116" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="111" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">7</text></g><g><circle cx="134" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="129" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">8</text></g><g><circle cx="152" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="147" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">9</text></g><g><circle cx="170" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="165" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">10</text></g><g><circle cx="188" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="183" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">11</text></g><g><circle cx="206" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="201" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">12</text></g><g><circle cx="224" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="219" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">13</text></g><g><circle cx="242" cy="8" r="7" style="fill: white; stroke: black; stroke-width: 1;"></circle><text dx="237" dy="11" class="label" style="font-size: 10px; font-weight: 500; display: none;">14</text><g class="label" style="display: none;"><rect x="126" y="1" width="14" height="14" style="fill: black;"></rect><text dx="130" dy="11" style="font-size: 9px; font-weight: 400; fill: white;">A</text></g></g></svg>
Alternatively, if you want to use width
, you shouldn't use getPointAtLength()
, as Lobe explains in his answer , because in Euclidean geometry, a straight line is shorter than any other geometric shape going from point A to point B.
Also, you have two problems: 1. There are many magic numbers all over the world, and 2. you can develop better mathematics for getting an arc. I think you should get rid of this code and write a completely new, different approach.
source to share