I have a running Angular 9 application where SVG has a spiral chart with min and max value for the degree.
I am using d3.js to plot given value of degree on the spiral chart.
I have written the following code :
// min -> min degree, -140 in this example
// max -> max degree, 440 in this example
// currentDegree -> degree value to be ploted, 0 in this example
// svg -> svg containing spiral chart
// circle -> circle to be moved to depict current Degree position in the svg
void setDegree(min,max,currentDegree, svg, circle) {
const pathNode = svg.select('path').node();
const totalPathLength = pathNode.getTotalLength();
const yDomain = d3.scale.linear().domain([min, max]).range(
[0, totalPathLength]);
const currentPathLength = yDomain(currentDegree); // current path length
const pathPoint = pathNode.getPointAtLength(totalPathLength - currentPathLength);
circle.transition()
.duration(300)
.attrTween('cx', () => (t) => pathPoint.x)
.attrTween('cy', () => (t) => pathPoint.y);
}
Above code produces this output :
In the above image, 0 degrees is slightly shifted to the right but it should have been at the center as shown in the image below :
function setDegree(min, max, currentDegree, svg, circle) {
const pathNode = svg.select("path").node();
const totalPathLength = pathNode.getTotalLength();
const yDomain = d3
.scaleLinear()
.domain([min, max])
.range([0, totalPathLength]);
const currentPathLength = yDomain(currentDegree); // current path length
const pathPoint = pathNode.getPointAtLength(
totalPathLength - currentPathLength
);
circle
.transition()
.duration(300)
.attrTween("cx", () => t => pathPoint.x)
.attrTween("cy", () => t => pathPoint.y);
}
const svg = d3.select("svg");
const circle = d3.select("#cur_pos");
setDegree(-140, 410, 0, svg, circle);
p {
font-family: Lato;
}
.cls-3 {
fill: none;
stroke-width: 10px;
stroke: #000;
}
.cls-3,
.cls-4,
.cls-5 {
stroke-miterlimit: 10;
}
.cls-4,
.cls-5 {
stroke-width: 0.25px;
}
.cls-5 {
font-size: 60px;
font-family: ArialMT, Arial;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1031.1 1010.3" preserveAspectRatio="xMinYMin meet">
<title>Spiral Chart</title>
<g>
<g id="Spiral_Chart">
<g id="Spiral_Path">
<path
class="cls-3 svg-range-line"
d="M881.5,154.9C679.6-47,352.3-47,150.4,154.9c-195.9,195.9-195.9,513.4,0,709.3,189.7,190,497.5,190.1,687.5.4l.4-.4c184.3-184.3,184.3-483,0-667.3h0C659.6,18.1,369.8,18.1,191.1,196.8H191C17.6,370.3,17.6,651.4,191,824.8"
/>
</g>
<circle
id="cur_pos"
class="cls-4 svg-range-indicator"
cx="514"
cy="64.3"
r="18.5"
/>
</g>
<text
id="Min"
class="cls-5 svg-text"
style="text-anchor:start;"
x="195"
y="880"
>
-140
</text>
<text
id="Max"
class="cls-5 svg-text"
style="text-anchor:start;"
x="885"
y="210"
>
410
</text>
</g>
</svg>
Because your spiral is smaller on the inside, if you calculate the length at 0 degrees (or 90, or -90), you'll overshoot it. That is because the total length of the path includes the outside part of the spiral, which is longer, because it's radius is greater. In other words, your logic is correct if the path would have been completely circular. But it's not so you're off by a little bit.
Note that if you change currentDegree to 360, it's almost perfectly placed. That is again because of this radius.
I've used this wonderful package kld-intersections, which can calculate the intersecting points of two SVG shapes.
I first take the midpoint of the circle, then calculate some very long line in the direction I want the circle to have. I calculate the intersections of the path with that line, and I get back an array of intersections.
Now, to know whether to use the closest or the furthest intersection, I sort them by distance to the centre, and check how many times 360 fits between the minimum angle and the desired angle.
Note that the centre point is not perfect, that is why if you change it to -140, the circle will not be at the exact end position. Maybe you can improve on this or - if the design is stable, calculate the point by hand.
const {
ShapeInfo,
Intersection
} = KldIntersections;
function getCentroid(node) {
const bbox = node.getBBox();
return {
x: bbox.x + bbox.width / 2,
y: bbox.y + bbox.height / 2,
};
}
function setDegree(min, max, currentDegree, svg, circle) {
const pathNode = svg.select("path").node();
const centroid = getCentroid(pathNode);
const pathInfo = ShapeInfo.path(pathNode.getAttribute("d"));
// We need to draw a line from the centroid, at the angle we want the
// circle to have.
const currentRadian = (currentDegree / 180) * Math.PI - Math.PI / 2;
const lineEnd = {
// HACK: small offset so the line is never completely vertical
x: centroid.x + 1000 * Math.cos(currentRadian) + Math.random() * 0.01,
y: centroid.y + 1000 * Math.sin(currentRadian),
};
indicatorLine
.attr("x1", centroid.x)
.attr("y1", centroid.y)
.attr("x2", lineEnd.x)
.attr("y2", lineEnd.y);
const line = ShapeInfo.line([centroid.x, centroid.y], [lineEnd.x, lineEnd.y]);
const intersections = Intersection.intersect(pathInfo, line).points;
// Sort the points based on their distance to the centroid
intersections.forEach(
p => p.dist = Math.sqrt((p.x - centroid.x) ** 2 + (p.y - centroid.y) ** 2));
intersections.sort((a, b) => a.dist - b.dist);
// See which intersection we need.
// Iteratively go round the circle until we find the correct one
let i = 0;
while (min + 360 * (i + 1) <= currentDegree) {
i++;
}
const pathPoint = intersections[i];
circle
.attr("cx", pathPoint.x)
.attr("cy", pathPoint.y);
}
const svg = d3.select("svg");
const indicatorLine = svg.append("line").attr("stroke", "red");
const circle = d3.select("#cur_pos");
setDegree(-140, 410, 0, svg, circle);
d3.select("input").on("change", function() {
setDegree(-140, 410, +this.value, svg, circle);
});
p {
font-family: Lato;
}
.cls-3 {
fill: none;
stroke-width: 10px;
stroke: #000;
}
.cls-3,
.cls-4,
.cls-5 {
stroke-miterlimit: 10;
}
.cls-4,
.cls-5 {
stroke-width: 0.25px;
}
.cls-5 {
font-size: 60px;
font-family: ArialMT, Arial;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://unpkg.com/kld-intersections"></script>
<label>Value</label> <input type="number" value="0" min="-140" max="410"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1031.1 1010.3" preserveAspectRatio="xMinYMin meet">
<g>
<g id="Spiral_Chart">
<g id="Spiral_Path">
<path
class="cls-3 svg-range-line"
d="M881.5,154.9C679.6-47,352.3-47,150.4,154.9c-195.9,195.9-195.9,513.4,0,709.3,189.7,190,497.5,190.1,687.5.4l.4-.4c184.3-184.3,184.3-483,0-667.3h0C659.6,18.1,369.8,18.1,191.1,196.8H191C17.6,370.3,17.6,651.4,191,824.8"
/>
</g>
<circle
id="cur_pos"
class="cls-4 svg-range-indicator"
cx="514"
cy="64.3"
r="18.5"
/>
</g>
<text
id="Min"
class="cls-5 svg-text"
style="text-anchor:start;"
x="195"
y="880"
>
-140
</text>
<text
id="Max"
class="cls-5 svg-text"
style="text-anchor:start;"
x="885"
y="210"
>
410
</text>
</g>
</svg>
Related
I am doing a small assignment to find out the type of the triangle whether is equilateral, isosceles, or scalene based on the values a,b,c.
my problem is that I couldn't manage to convert the a,b,c values in order to draw the shape by using SVG (polygon) since it takes points and I have only values
function triangle(a, b, c) {
if (a === b && a === c && b === c) return "equilateral";
if (a === b && a === c) return "isosceles";
else {
return "scalene";
}
}
<div>
<svg className="traingle" height="400" width="400">
<polygon points={`200 ${b}, 200 ${a}, 0 ${c}`} />
</svg>
</div>
This question probably belongs in Maths StackExchange, but anyway.
Firstly, you need to first check whether it is possible for the combination of side lengths to form a triangle.
The way to do that is with the Triangle Inequality Theorem. It states that each of the side lengths must be less than the sum of the other two.
So in your example, the lengths [5,200,300] cannot form a triangle because 300 is not less than (5 + 200).
Also, none of the lengths should be <= 0.
So let's work with three valid side lengths: [150, 200, 300].
The way to get the three points is to start with one side, and draw that first. So we will start with the side that is 150.
You can draw that side anywhere, but let's make the line go from (0,0) to (150,0).
To get the third point we need to find where the other two sides would meet. Imagine two circles centred at each end of that first line. Each circle has a radius corresponding to the remaining two side lengths. The way to calculate the third point is to find the instersection point(s) of those two circles.
svg {
background-color: linen;
width: 500px;
}
line {
stroke: black;
stroke-width: 2;
}
.c300 {
fill: none;
stroke: blue;
stroke-width: 2;
}
.c200 {
fill: none;
stroke: green;
stroke-width: 2;
}
line.la {
stroke: blue;
stroke-dasharray: 4 6;
}
.lb {
stroke: green;
stroke-dasharray: 4 6;
}
<svg viewBox="-350 -350 750 700">
<line class="l150" x1="0" y1="0" x2="150" y2="0"/>
<circle class="c300" cx="0" cy="0" r="300"/>
<circle class="c200" cx="150" cy="0" r="200"/>
<line class="la" x1="0" y1="0" x2="241.667" y2="-177.756"/>
<line class="lb" x1="150" y1="0" x2="241.667" y2="-177.756"/>
</svg>
If we use Wolfram Alpha's version of the formula then we get the following:
d^2 - r^2 + R^2
x = -----------------
2 * d;
and
sqrt( 4 * d^2 * R^2 - ( d^2 - r^2 + R^2 )^2 )
y = -----------------------------------------------
2 * d
where d is the length of that first side, and r and R are the lengths of the other two sides.
So for our example:
R = 300
r = 200
d^2 - r^2 + R^2 = 150^2 - 300^2 + 200^2
= 72500
x = 72500 / 300
= 241.667
y = sqrt( 4 * 22500 * 90000 - 72500^2) / 300
= sqrt( 2843750000 ) / 300
= 53326.823 / 300
= 177.756
And you can see these values for x and y in the example SVG above.
Note that the circles intersect both above and below the first line. So 177.756 and -177.656 are both valid solutions for y.
Let's assume point A is always at 0,0, and point B has the same y coordinate. You can calculate position of C using the law of cosines:
const calculateTriangle = (a, b, c) => {
const ax = 0;
const ay = 0;
const bx = ax + c;
const by = ay;
const cosAlpha = (a * a - b * b - c * c) / (b * c * 2);
const alpha = Math.acos(cosAlpha);
const cx = ax - b * cosAlpha;
const cy = ay + b * Math.sin(alpha);
return {ax, ay, bx, by, cx, cy};
};
Here is a working snippet with calculateTriangle. Please note that the side length numbers should be valid
const calculateTriangle = (a, b, c) => {
const ax = 0;
const ay = 0;
const bx = ax + c;
const by = ay;
const cosAlpha = (a * a - b * b - c * c) / (b * c * 2);
const alpha = Math.acos(cosAlpha);
const cx = ax - b * cosAlpha;
const cy = ay + b * Math.sin(alpha);
return {ax, ay, bx, by, cx, cy};
};
const onDraw = () => {
const a = d3.select('#side-a').node().value;
const b = d3.select('#side-b').node().value;
const c = d3.select('#side-c').node().value;
const {ax, ay, bx, by, cx, cy} = calculateTriangle(parseInt(a), parseInt(b), parseInt(c));
d3.select('polygon').attr('points', `${ax},${ay} ${bx},${by} ${cx},${cy}`)
console.log('T: ', t);
}
d3.select('button').on('click', onDraw);
#wrapper {
display: flex;
flex-direction: row;
}
#container {
font-family: Ubuntu;
font-size: 16px;
display: flex:
flex-direction: column;
}
#container > div {
display: flex;
flex-direction: row;
margin: 10px 0 0 20px;
}
#container > div > span {
width: 10px;
}
#container > div > input {
margin-left: 20px;
width: 50px;
}
#container button {
margin-left: 30px;
width: 60px;
}
svg {
border: 1px solid grey;
margin: 10px 0 0 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id='wrapper'>
<div id='container'>
<div>
<span>A: </span><input type="number" id="side-a" value="50"/>
</div>
<div>
<span>B: </span><input type="number" id="side-b" value="40"/>
</div>
<div>
<span>C: </span><input type="number" id="side-c" value="30"/>
</div>
<div>
<button>DRAW</button>
</div>
</div>
<svg width="110" height="110">
<polygon transform="translate(10,10)" />
</svg>
</div>
I'm attempting to create a crude database diagram generator using D3, but I can't figure out how to get connectors between fields. I can get straight lines going from two points, but I wanted it to be rounded and like a path I guess.
I've tried to put together an example of just that specific issue, linking two text fields:
https://codesandbox.io/s/gifted-bardeen-5hbw2?fontsize=14&hidenavigation=1&theme=dark
Here's an example from dbdiagram.io of what I'm referring to:
I've been reading up on the d attribute and the various commands, but nothing seems even close. I suspect the forceSimulation method, especially the forceCenter function might be messing up the relative positioning when I use the lower-cased commands. But not 100% on that.
You can compute a connector path between 2 points by connectorPath routine:
const source = {x: 200, y: 120};
const target = {x: 50, y: 20};
const MAX_RADIUS = 15;
const connectorPath = (from, to) => {
if (from.y === to.y || from.x === to.x)
return `M ${from.x},${from.y} L ${to.x},${to.y}`;
const middle = (from.x + to.x) / 2;
const xFlag = from.x < to.x ? 1 : -1;
const yFlag = from.y < to.y ? 1 : -1;
const dX = Math.abs(from.x - to.x);
const dY = Math.abs(from.y - to.y);
const radius = Math.min(dX / 2, dY / 2, MAX_RADIUS);
return `M ${from.x},${from.y} H ${middle - radius * xFlag} Q ${middle},${from.y} ${middle},${from.y + radius * yFlag} V ${to.y - radius * yFlag} Q ${middle},${to.y} ${middle + radius * xFlag},${to.y} H ${to.x}`;
};
d3.select('#source').attr('cx', source.x).attr('cy', source.y);
d3.select('#target').attr('cx', target.x).attr('cy', target.y);
d3.select('#connector').attr('d', connectorPath(source, target));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="300" height="200">
<path id="connector" stroke="blue" fill="none" />
<circle id="source" fill="red" r="5"/>
<circle id="target" fill="green" r="5"/>
</svg>
I am in the process of trying to create a 30s countdown timer display using SVG and a spot of JS. The idea is simple
Draw the face of the countdown clock as an SVG circle
Inside it draw a closed SVG path in the form of the sector of circle
Use window.requestAnimationFrame to update that sector at one second intervals
My effort is shown below. While it works the final result is far from being smooth and convincing.
When the spent time gets into the second quadrant of the circle the sector appears to swell past the circumference
When it is in the third and fourth quadrant it appears to detach from the circumference.
What am I doing wrong here and how could it be improved?
var _hold = {tickStart:0,stopTime:30,lastDelta:0};
String.prototype.format = function (args)
{
var newStr = this,key;
for (key in args) {newStr = newStr.replace('{' + key + '}',args[key]);}
return newStr;
};
Boolean.prototype.intval = function(places)
{
places = ('undefined' == typeof(places))?0:places;
return (~~this) << places;
};
function adjustSpent(timeStamp)
{
if (0 === _hold.tickStart) _hold.tickStart = timeStamp;
var delta = Math.trunc((timeStamp - _hold.tickStart)/1000);
if (_hold.lastDelta < delta)
{
_hold.lastDelta = delta;
var angle = 2*Math.PI*(delta/_hold.stopTime),
dAngle = 57.2958*angle,
cx = cy = 50,
radius = 38,
top = 12,
x = cx + radius*Math.sin(angle),
y = cy - radius*Math.cos(angle),
large = (180 < dAngle).intval();
var d = (360 <= dAngle)?"M50,50 L50,12 A38,38 1 0,1 51,12 z":"M50,50 L50,12 A38,38 1 {ll},1 {xx},{yy} z".format({ll:large,xx:x,yy:y});
var spent = document.getElementById('spent');
if (spent) spent.setAttribute("d",d);
}
if (delta < _hold.stopTime) window.requestAnimationFrame(adjustSpent);
}
window.requestAnimationFrame(adjustSpent);
timer
{
position:absolute;
height:20vh;
width:20vh;
border-radius:100%;
background-color:orange;
left:calc(50vw - 5vh);
top:15vh;
}
#clockface{fill:white;}
#spent{fill:#6683C2;}
<timer>
<svg width="20vh" height="20vh" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="38" id="clockface"></circle>
<path d="M50,50 L50,12 A38,38 1 0,1 51,12 z" id="spent"></path>
</svg>
</timer>
A posible solution would be using a stroke animation like this:
The blue circle has a radius of 38/2 = 19
The stroke-width of the blue circle is 38 giving the illusion of a circle of 38 units.
Please take a look at the path: it's also a circle of radius = 19.
svg {
border: 1px solid;
height:90vh;
}
#clockface {
fill: silver;
}
#spent {
fill:none;
stroke: #6683c2;
stroke-width: 38px;
stroke-dasharray: 119.397px;
stroke-dashoffset: 119.397px;
animation: dash 5s linear infinite;
}
#keyframes dash {
to {
stroke-dashoffset: 0;
}
}
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="38" id="clockface"></circle>
<path d="M50,31 A19,19 1 0,1 50,69 A19,19 1 0,1 50,31" id="spent"></path>
</svg>
In this case I've used css animations but you can control the value for stroke-dashoffset with JavaScript.
The value for stroke-dasharray was obtained using spent.getTotalLength()
If you are not aquainted with stroke animations in SVG please read How SVG Line Animation Works
I have to implement the idea of a circle approximation that is a regular polygon with N corners, whereas N is defined by the user.
For example, if N=3 I would have a triangle. With n=5 I would a shape that starts resembling a circle. As I increase N, I would get closer and closer to the shape of a circle.
This idea it's very similar to what was asked and answered on the on the follwing question/solution:
Draw regular polygons inscribed in a circle , however, they used raphael.js and not D3.js.
What I tried to do:
var vis = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 667);
var svg = d3.select('svg');
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var outerCircle = svg.append("circle").attr({
cx: originX,
cy: originY,
r: outerCircleRadius,
fill: "none",
stroke: "black"
});
var chairWidth = 10;
var chairOriginX = originX + ((outerCircleRadius) * Math.sin(0));
var chairOriginY = originY - ((outerCircleRadius) * Math.cos(0));
var chair = svg.append("rect").attr({
x: chairOriginX - (chairWidth / 2),
y: chairOriginY - (chairWidth / 2),
width: chairWidth,
opacity: 1,
height: 20,
fill: "none",
stroke: "blue"
});
var n_number = 5
var n_angles = 360/n_number
var angle_start=0;
var angle_next;
console.log(chair.node().getBBox().x);
console.log(chair.node().getBBox().y);
chair.attr("transform", "rotate(" + (angle_start+n_angles+n_angles) + ", 200, 200)");
var circle = svg.append("circle")
.attr("cx", 195)
.attr("cy", 135)
.attr("r", 50)
.attr("fill", "red");
var chairOriginX2 = originX + ((outerCircleRadius) * Math.sin(0));
var chairOriginY2 = originY - ((outerCircleRadius) * Math.cos(0));
var chair2 = svg.append("rect").attr({
x: chairOriginX2 - (chairWidth / 2),
y: chairOriginY2 - (chairWidth / 2),
width: chairWidth,
opacity: 1,
height: 20,
fill: "none",
stroke: "blue"
});
console.log(chair2.node().getBBox().x);
console.log(chair2.node().getBBox().y);
My idea, that did not work, was trying to create a circle ("outerCircle") which I would slide within the circunference ("chair.attr("transform"...") of the circle, based on N, obtaining several different (x,y) coordinates.
Then, I would feed (x,y) coordinates to a polygon.
I believe that my approach for this problem is wrong. Also, the part that I got stuck is that I am not being able to keep sliding whitin the circunference and storing each different (x,y) coordinate. I tried "console.log(chair2.node().getBBox().x);" but it is always storing the same coordinate, which it is of the origin.
I've simplified your code for clarity. To get the x of a point on a circle you use the Math.cos(angle) and for the y you use the Math.sin(angle). This was your error. Now you can change the value of the n_number
var SVG_NS = 'http://www.w3.org/2000/svg';
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var polygon = document.createElementNS(SVG_NS, 'polygon');
svg.appendChild(polygon);
let points="";
var n_number = 5;
var n_angles = 2*Math.PI/n_number
// building the value of the `points` attribute for the polygon
for(let i = 0; i < n_number; i++){
let x = originX + outerCircleRadius * Math.cos(i*n_angles);
let y = originY + outerCircleRadius * Math.sin(i*n_angles);
points += ` ${x},${y} `;
}
// setting the value of the points attribute of the polygon
polygon.setAttributeNS(null,"points",points)
svg{border:1px solid;width:90vh;}
polygon{fill: none;
stroke: blue}
<svg id="svg" viewBox = "100 100 200 200" >
<circle cx="200" cy="200" r="60" fill="none" stroke="black" />
</svg>
This is another demo where I'm using an input type range to change the n_numbervariable
var SVG_NS = 'http://www.w3.org/2000/svg';
var originX = 200;
var originY = 200;
var outerCircleRadius = 60;
var polygon = document.createElementNS(SVG_NS, 'polygon');
svg.appendChild(polygon);
let points="";
var n_number = 5;
setPoints(n_number);
theRange.addEventListener("input", ()=>{
n_number = theRange.value;
setPoints(n_number)
});
function setPoints(n_number){
var n_angles = 2*Math.PI/n_number;
points = ""
// building the value of the `points` attribute for the polygon
for(let i = 0; i < n_number; i++){
let x = originX + outerCircleRadius * Math.cos(i*n_angles);
let y = originY + outerCircleRadius * Math.sin(i*n_angles);
points += ` ${x},${y} `;
}
// setting the value of the points attribute of the polygon
polygon.setAttributeNS(null,"points",points);
}
svg{border:1px solid; width:90vh;}
polygon{fill: none;
stroke: blue}
<p><input type="range" min="3" max="50" value="5" id="theRange" /></p>
<svg id="svg" viewBox = "100 100 200 200" >
<circle cx="200" cy="200" r="60" fill="none" stroke="black" />
</svg>
The answer provided by enxaneta is perfectly fine and is certainly the classical approach to this. However, I often favor letting the browser do the trigonometry instead of doing it on my own. Typical examples include my answer to "Complex circle diagram" or the one to "SVG marker - can I set length and angle?". I am not even sure if they outperform the more classical ones but I like them for their simplicity nonetheless.
My solution focuses on the SVGGeometryElement and its methods .getTotalLength() and .getPointAtLength(). Since the SVGCircleElement interface extends that interface those methods are available for an SVG circle having the following meanings:
.getTotalLength(): The circumference of the circle.
.getPointAtLength(): The point in x-/y-coordinates on the circle at the given length. The measurement per definition begins at the 3 o'clock position and progresses clockwise.
Given these explanations it becomes apparent that you can divide the circle's total length, i.e. its circumference, by the number of points for your approximation. This gives you the step distance along the circle to the next point. By summing up these distances you can use the second method to obtain the x-/y-coordinates for each point.
The coding could be done along the following lines:
// Calculate step length as circumference / number of points.
const step = circleElement.getTotalLength() / count;
// Build an array of points on the circle.
const data = Array.from({length: count}, (_, i) => {
const point = circleElement.getPointAtLength(i * step); // Get coordinates of next point.
return `${point.x},${point.y}`;
});
polygon.attr("points", data.join(" "));
Slick and easy! No trigonometry involved.
Finally, a complete working demo:
// Just setup, not related to problem.
const svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
const circle = svg.append("circle")
.attr("cx", "150")
.attr("cy", "150")
.attr("r", "100")
.attr("fill", "none")
.attr("stroke", "black");
const polygon = svg.append("polygon")
.attr("fill", "none")
.attr("stroke", "blue");
const circleElement = circle.node();
const ranger = d3.select("#ranger").on("input", update);
const label = d3.select("label");
// This function contains all the relevant logic.
function update() {
let count = ranger.node().value;
label.text(count);
// Calculate step length as circumference / number of points.
const step = circleElement.getTotalLength() / count;
// Build an array of all points on the circle.
const data = Array.from({length: count}, (_, i) => {
const point = circleElement.getPointAtLength(i * step); // Get coordinates of next point.
return `${point.x},${point.y}`;
});
polygon.attr("points", data.join(" "));
}
update();
<script src="https://d3js.org/d3.v5.js"></script>
<p>
<input id="ranger" type="range" min="3" max="15" value="5">
<label for="ranger"></label>
</p>
I want to connect two SVG points (e.g. the centers of two circles) using arcs. If there is only one connection, the line (<path>) will be straight. If there are two connections, both will be rounded and will be symmetrical, this way:
So, in fact, there are few rules:
Everything should be symmetrical to to the imaginary line that connects the two points.
From 1, it's obvious that if the number of connections is:
odd: we do not display the straight line
even: we display the straight line
There should be a value k which defines the distance between two connections between same points.
The tangent that goes through the middle of the elliptical arc should be parallel with the straight line that connects the two points. And obviously, the middle of the line will be perpendicular to the tangent.
I'm struggling to get a formula to calculate the A parameters in the <path> element.
What I did until now is:
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent"/>
M100 100 is clear: that's the starting point (move to 100,100)
Last two numbers are also clear. The path ends in 300,100
I also saw that if I put 0 instead of 20, I obtain a straight line.
If I replace 1,0 with 1,1, the path is flipped.
What I don't know is how to calculate the A parameters. I read the docs, but the imagine is still unclear to me. How to calculate these values?
svg {
width: 100%;
height: 100%;
position: absolute;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<?xml version="1.0" standalone="no" ?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<!-- Connect A(100,100) with B(300, 100) -->
<path d="M100 100, A50,0 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 0 1,1 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,0 300,100" stroke="black" fill="transparent" />
<path d="M100 100, A50,30 0 1,1 300,100" stroke="black" fill="transparent" />
<!-- A(100, 100) B(300, 400) -->
<path d="M100 100, A50,0 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,0 300,400" stroke="black" fill="transparent" />
<path d="M100 100, A50,20 57 1,1 300,400" stroke="black" fill="transparent" />
</svg>
</body>
</html>
I'm using SVG.js to create the paths.
You're making life very difficult for yourself by requiring circular arcs.
If you use quadratic curves instead, then the geometry becomes very simple — just offset the central X coordinate by half the difference in Y coordinates, and vice versa.
function arc_links(dwg,x1,y1,x2,y2,n,k) {
var cx = (x1+x2)/2;
var cy = (y1+y2)/2;
var dx = (x2-x1)/2;
var dy = (y2-y1)/2;
var i;
for (i=0; i<n; i++) {
if (i==(n-1)/2) {
dwg.line(x1,y1,x2,y2).stroke({width:1}).fill('none');
}
else {
dd = Math.sqrt(dx*dx+dy*dy);
ex = cx + dy/dd * k * (i-(n-1)/2);
ey = cy - dx/dd * k * (i-(n-1)/2);
dwg.path("M"+x1+" "+y1+"Q"+ex+" "+ey+" "+x2+" "+y2).stroke({width:1}).fill('none');
}
}
}
function create_svg() {
var draw = SVG('drawing').size(300, 300);
arc_links(draw,50,50,250,50,2,40);
arc_links(draw,250,50,250,250,3,40);
arc_links(draw,250,250,50,250,4,40);
arc_links(draw,50,250,50,50,5,40);
draw.circle(50).move(25,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,25).fill('#fff').stroke({width:1});
draw.circle(50).move(225,225).fill('#fff').stroke({width:1});
draw.circle(50).move(25,225).fill('#fff').stroke({width:1});
}
create_svg();
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.3.2/svg.min.js"></script>
<div id="drawing"></div>
For drawing SVG path's arc you need 2 points and radius, there are 2 points and you just need to calculate radius for given distances.
Formula for radius:
let r = (d, x) => 0.125*d*d/x + x/2;
where:
d - distance between points
x - distance between arcs
it derived from Pythagorean theorem:
a here is a half of distance between points
let r = (d, x) => !x?1e10:0.125*d*d/x + x/2;
upd();
function upd() {
let n = +count.value;
let s = +step.value/10;
let x1 = c1.getAttribute('cx'), y1 = c1.getAttribute('cy');
let x2 = c2.getAttribute('cx'), y2 = c2.getAttribute('cy');
let dx = Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
paths.innerHTML = [...Array(n)].map((_, i) => [
n%2&&i===n-1?0:1+parseInt(i/2),
i%2
]).map(i => `<path d="${[
'M', x1, y1,
'A', r(dx, s*i[0]), r(dx, s*i[0]), 0, 0, i[1], x2, y2
].join(' ')}"></path>`).join('');
}
<input id="count" type="range" min=1 max=9 value=5 oninput=upd() >
<input id="step" type="range" min=1 max=200 value=100 oninput=upd() >
<svg viewbox=0,0,300,100 stroke=red fill=none >
<circle id=c1 r=10 cx=50 cy=60></circle>
<circle id=c2 r=10 cx=250 cy=40></circle>
<g id=paths></g>
</svg>
Here is a solution that uses arcs, as asked for, rather than quadratic curves.
// Internal function
function connectInternal(x1,y1,x2,y2,con){
var dx=x2-x1
var dy=y2-y1
var dist=Math.sqrt(dx*dx+dy*dy)
if(dist==0 || con==0){
return "M"+x1+","+y1+"L"+x2+","+y2
}
var xRadius=dist*0.75
var yRadius=dist*0.3*(con*0.75)
var normdx=dx/dist
if(normdx<-1)normdx=-1
if(normdx>1)normdx=1
var angle=Math.acos(normdx)*180/Math.PI
if(x1>x2){
angle=-angle
}
return "M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",00"+x2+","+y2+
"M"+x1+","+y1+"A"+xRadius+","+yRadius+","+
angle+",01"+x2+","+y2
}
// Returns an SVG path that represents
// "n" connections between two points.
function connect(x1,y1,x2,y2,n){
var ret=""
var con=n
if(con%2==1){
ret+=connectInternal(x1,y1,x2,y2,con)
con-=1
}
for(var i=2;i<=con;i+=2){
ret+=connectInternal(x1,y1,x2,y2,i)
}
return ret
}