How to Create a path in svg for d3 brush - javascript

here what i having a svg like below
currently svg code is like this
<path class="c" d="M-8046.012,2842.011h-1.6" transform="translate(8047.61 -2837.554)"/>
</g></g></g></svg>
and currently this is using groups and path combined and this i want to attach as a d3 brush handle but here the problem is currently im creating a brush handle like below
const focusHandle = focusBrush.selectAll(".handle--custom")
.data([{type: "w"}, {type: "e"}])
.enter().append("path")
.attr("class", "handle--custom")
.attr("stroke", "#000")
.attr("cursor", "ew-resize")
.attr("d", brushResizePath)
const brushResizePath = (d) => {
var e = +(d.type == "e"),
x = e ? 1 : -1,
y = this.height / 2;
return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) + "M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8);
}
ex: "M0.5,54A6,6 0 0 1 6.5,60V102A6,6 0 0 1 0.5,108ZM2.5,62V100M4.5,62V100" like path
so how can i implement the above brush handle to this
Currently my brush is like this

I found a few issues with your code here.
Arrow functions don't assign this, so this in the arrow function is actually the window, I'm not sure if that's intended or not. If you want this to be the path, you need to use a function block instead of an arrow function.
Neither the window nor the paths have a height attribute, I think you want innerHeight instead. If you look at the d attribute that gets set on the path in you'll see there's some NaNs where you're trying to use y.
brushResizePath is being used before it's defined, move the definition above the const focusHandle bit.
Here's my CodePen with it working: https://codepen.io/Thource/pen/RwWqmZj

Related

Draw small symmetrical arc line path between two markers on world map in Highmaps

I'm using Highmaps to create a flight path chart, using their demo Simple flight routes (jsfiddle) as a starting point. When I update the code to use a world map the line paths between locations/markers become distorted with an exaggerated curve.
See my jsfiddle where I have only modified from the demo to the following:
HTML
<!-- line 5 -->
<script src="https://code.highcharts.com/mapdata/custom/world.js"></script>
JavaScript
// line 39
mapData: Highcharts.maps['custom/world'],
// line 47
data: Highcharts.geojson(Highcharts.maps['custom/world'], 'mapline'),
See the difference between both where before is the Highcharts demo and after is my few changes as noted above:
The paths are calculated through function pointsToPath which uses quadratic Bézier curve Q in SVG to curve the line drawn between markers.
// Function to return an SVG path between two points, with an arc
function pointsToPath(from, to, invertArc) {
var arcPointX = (from.x + to.x) / (invertArc ? 2.4 : 1.6),
arcPointY = (from.y + to.y) / (invertArc ? 2.4 : 1.6);
return 'M' + from.x + ',' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
',' + to.x + ' ' + to.y;
}
If I modify the function to always divide by 2 for arc points x and y then I get a straight line between markers:
var arcPointX = (from.x + to.x) / 2,
arcPointY = (from.y + to.y) / 2;
I'm not sure of the math to get a smaller, less exaggerated curve.
Ideally I would like for the line to be symmetrical as per an example from MDN - Paths:
<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 Q 95 10 180 80" stroke="black" fill="transparent"/>
</svg>
Using world map data, how do I calculate line paths between markers to appear with a smaller or symmetrical curve?
You just need to get your denominator closer to 2.0 as when it is 2.0 is a perfectly straight line: https://jsfiddle.net/my7bx50p/1/
So I chose 2.03 and 1.97 and that gives you much "softer" curves. Hope that helps.
function pointsToPath(from, to, invertArc) {
var arcPointX = (from.x + to.x) / (invertArc ? 2.03 : 1.97),
arcPointY = (from.y + to.y) / (invertArc ? 2.03 : 1.97);
return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY + ' ' + to.x + ' ' + to.y;
}
UPDATE:
I tried to focus in only on the math: https://jsfiddle.net/9gkvhfuL/1/
I think the math is now correct:
Which back to the real example: https://jsfiddle.net/my7bx50p/6/
Gives, I believe, the desired outcome :):
From the code (https://jsfiddle.net/my7bx50p/6/):
function pointsToPath(from, to, invertArc) {
var centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
var slope = (to.x - from.x) / (to.y - from.y);
var invSlope = -1 / slope;
var distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );
if (Math.abs(slope) > Math.abs(invSlope) ){
//then we should offset in the y direction
var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
var min_slope = Math.min( Math.abs(slope), Math.abs(invSlope) );
var final_slope = Math.max(min_slope, 1);
var offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];
//console.log(centerPoint, slope, invSlope, distance);
var arcPointX = offsetCenter[0], //(from.x + to.x) / (invertArc ? 2.03 : 1.97),
arcPointY = offsetCenter[1] //(from.y + to.y) / (invertArc ? 2.03 : 1.97);
} else{ //invSlope <= slope
//then we should offset in the x direction
var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
var min_slope = Math.min( Math.abs(slope), Math.abs(invSlope) );
var final_slope = Math.max(min_slope, 1);
var offsetCenter = [centerPoint[0] + offset, centerPoint[1] + (offset * (1/invSlope))];
//console.log(centerPoint, slope, invSlope, distance);
var arcPointX = offsetCenter[0], //(from.x + to.x) / (invertArc ? 2.03 : 1.97),
arcPointY = offsetCenter[1] //(from.y + to.y) / (invertArc ? 2.03 : 1.97);
}
return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
' ' + to.x + ' ' + to.y;
}
UPDATE 2: (to try to explain the math and clean up the code)
Check out the Math Fiddle: https://jsfiddle.net/alexander_L/dcormfxy/53/
The solid black line between the two points is the straight line between them and it also has the corresponding slope (used in the code later). I also drew a centre point on each line. Then I drew the inverse slope as a dotted line (also used in the code) The inverse slope is by definition perpendicular to the slope and is related by invSlope = -1/slope. From this, we are now set up to find the perpendicular points to the left or right of the centre point, that will become the centre of our symmetrical arcs. We do this by first figuring out if the slope is greater than the inverse slope or if the inverse slope is greater than the slope (absolute values). This is only necessary because when we have a perfectly horizontal or perfectly vertical line then the slope is zero and undefined respectively and then our math doesn't work. (remember slope = (y2 - y1)/(x2 - x1) so when the line is vertical y-changes but x-doesn't so x2 = x1 and then the denominator is zero and gives us undefined slope)
Let's think about line C from : {x: 40, y: 40}, to : {x: 220, y: 40}
slope = (y2 - y1)/(x2 - x1)
slope = (40 - 40)/(220 - 40)
slope = 0 / 180
slope = 0
invSlope = -1/slope
invSlope = undefined
This is why we need to have the two cases (the if else) in the code as whenever we get slope or invSlope as undefined the math is not going to work. So now, although slope is zero, it is greater than invSlope (undefined). (note SVGs are upside down compared to normal charts and how we think about them so it requires your brain to keep that in mind, otherwise, it's easy to get lost)
So now we can offset the centre point in the y-direction and then figure out how much we have to offset in the x-direction. If you had a line with slope 1, then you would offset the same in the x and the y-directions because the slope of the line is 1 (line makes a 45-degree angle with the x-axis) and so moving perpendicularly away from that line is achieved just by moving for example 5 in the x-direction and -5 in the y-direction.
Luckily, with this edge case (slope = 0), then we just move in the y-direction and the x-direction offset = 0. Look at line C in the math example and you can see what I mean, to move perpendicularly, we just move either positive or negative y-direction from the centre point. From the code:
offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];
So as I said, we offset in the y-direction from the centerPoint and the term + (offset * (1/slope)) will be zero here because 1/slope is undefined. We can chose to offset "left" or "right" by the function's argument invertArc which is used in this line: var offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance); which basically means move postive or negative direction away from the centerPoint in a magnitude equal to two times the square root of the distance between the points. I settled on two times the square root of the distance between the points because this gives us an offsetCenter of our arc that gives similarly soft curves for all lines both short and long.
Now, let's think about line A from : {x: 40, y: 40}, to : {x: 320, y: 360}
slope = (y2 - y1)/(x2 - x1)
slope = (360 - 40)/(320 - 40)
slope = 320 / 280
slope = 1.143
invSlope = -1/slope
invSlope = -0.875
Final cleaned up code and real example here https://jsfiddle.net/alexander_L/o43ka9u5/4/:
function pointsToPath(from, to, invertArc) {
var centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
var slope = (to.x - from.x) / (to.y - from.y);
var invSlope = -1 / slope;
var distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );
var arcPointX = 0;
var arcPointY = 0;
var offset = 0;
var offsetCenter = 0;
if (Math.abs(slope) > Math.abs(invSlope) ){
//then we should offset in the y direction (then calc. x-offset)
offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
offsetCenter = [centerPoint[0] + (offset * (1/slope)), centerPoint[1] + offset];
arcPointX = offsetCenter[0]
arcPointY = offsetCenter[1]
} else{ //invSlope >= slope
//then we should offset in the x direction (then calc. y-offset)
offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
offsetCenter = [centerPoint[0] + offset, centerPoint[1] + (offset * (1/invSlope))];
arcPointX = offsetCenter[0]
arcPointY = offsetCenter[1]
}
return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
' ' + to.x + ' ' + to.y;
}
UPDATE 3:
I figured out how to remove the need for the if else control flow / switch statement by using trigonometry instead. I hope my sketch helps explain the logic, you might also want to read some stuff (https://study.com/academy/lesson/sohcahtoa-definition-example-problems-quiz.html) etc. as I would struggle to explain briefly here (and I am already writing an essay here :) so will not explain SOH CAH TOA etc.)
This makes the core function code like this (Math only - https://jsfiddle.net/alexander_L/dcormfxy/107/) (full example - https://jsfiddle.net/alexander_L/o43ka9u5/6/):
function pointsToPath(from, to, invertArc) {
const centerPoint = [ (from.x + to.x) / 2, (from.y + to.y) / 2];
const slope = (to.y - from.y) / (to.x - from.x);
const invSlope = -1 / slope;
const distance = Math.sqrt( Math.pow((to.x - from.x), 2) + Math.pow((to.y - from.y), 2) );
const offset = (invertArc ? -1 : 1) * 2 * Math.sqrt(distance);
const angle = Math.atan(slope);
//Math.cos(angle) = offsetY/offset;
//Math.sin(angle) = offsetX/offset;
const offsetY = Math.cos(angle)*offset;
const offsetX = Math.sin(angle)*offset;
//if slope = 0 then effectively only offset y-direction
const offsetCenter = [centerPoint[0] - offsetX, centerPoint[1] + offsetY];
const arcPointX = offsetCenter[0]
const arcPointY = offsetCenter[1]
return 'M' + from.x + ' ' + from.y + 'Q' + arcPointX + ' ' + arcPointY +
' ' + to.x + ' ' + to.y;
}
I believe this code is more elegant, cleaner and more robust and mathematically valid :) Thanks also Ash for the tips on Const & Let use versus var.
This also gives the final result:

How to remove data cleanly before updating d3.js chart?

I'm quite new to D3 and been working through trying to figure everything out. I'm trying to configure this example here to update with new data and transition appropriately.
Here is the code pen I have configured (click the submit to update)
http://codepen.io/anon/pen/pbjLRW?editors=1010
From what I can gather, using some variation of .exit() is required for a clean data transition, but after reading some tutorials I'm still finding it difficult to know how it works. I have seen examples where simply removing the containers before calling the draw function works, but in my limited experience it can cause a flicker when changing data so I'm not sure if it's best practice?
Now, I'm not sure why the data is not updating correctly in my codepen, but my main concern is trying to get the transition right. Ideally I would like to know how I could just move the needle when changing data, so It would go from 90 > 40 for example, instead of 90 > 0 > 40.
However, I will definitely settle for figuring out why it doesn't redraw itself in the same location once clicking submit in the linked codepen.
Here is my update function;
function updateGuage() {
d3.selectAll("text").remove()
d3.selectAll('.needle').remove()
chart.remove()
name = "qwerty";
value = "25";
drawGuage();
}
initial draw;
function drawGuage() {
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
chart.append('path').attr('class', "arc chart-first");
chart.append('path').attr('class', "arc chart-second");
chart.append('path').attr('class', "arc chart-third");
arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
repaintGauge = function() {
perc = 0.5;
var next_start = totalPercent;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc1.startAngle(arcStartRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
next_start += perc / 3;
arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 3);
arc3.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
chart.select(".chart-third").attr('d', arc3);
}
/////////
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function() {
return dataset[0].metric;
})
.attr('id', "Name")
.attr('transform', "translate(" + ((width + margin.left) / 6) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
var trX = 180 - 210 * Math.cos(percToRad(percent / 2));
var trY = 195 - 210 * Math.sin(percToRad(percent / 2));
// (180, 195) are the coordinates of the center of the gauge.
displayValue = function() {
texts.append("text")
.text(function() {
return dataset[0].value;
})
.attr('id', "Value")
.attr('transform', "translate(" + trX + ", " + trY + ")")
.attr("font-size", 18)
.style("fill", '#000000');
}
texts.append("text")
.text(function() {
return 0;
})
.attr('id', 'scale0')
.attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue / 2;
})
.attr('id', 'scale10')
.attr('transform', "translate(" + ((width + margin.left) / 2.15) + ", " + ((height + margin.top) / 30) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue;
})
.attr('id', 'scale20')
.attr('transform', "translate(" + ((width + margin.left) / 1.03) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
var Needle = (function() {
//Helper function that returns the `d` value for moving the needle
var recalcPointerPos = function(perc) {
var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
thetaRad = percToRad(perc / 2);
centerX = 0;
centerY = 0;
topX = centerX - this.len * Math.cos(thetaRad);
topY = centerY - this.len * Math.sin(thetaRad);
leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);
return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
};
function Needle(el) {
this.el = el;
this.len = width / 2.5;
this.radius = this.len / 8;
}
Needle.prototype.render = function() {
this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);
return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));
};
Needle.prototype.moveTo = function(perc) {
var self,
oldValue = this.perc || 0;
this.perc = perc;
self = this;
// Reset pointer position
this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
return function(percentOfPercent) {
var progress = (1 - percentOfPercent) * oldValue;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
return function(percentOfPercent) {
var progress = percentOfPercent * perc;
repaintGauge(progress);
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
};
return Needle;
})();
needle = new Needle(chart);
needle.render();
needle.moveTo(percent);
setTimeout(displayValue, 1350);
}
Any help/advice is much appreciated,
Thanks
What you wanna check out is How selections work written by Mike Bostock. After reading this article, everything around enter, update and exit selections will become clearer.
In a nutshell:
You create a selection of elements with selectAll('li')
You join the selection with a data array through calling data([...])
Now D3 compares what's already in the DOM with the joined data. Each DOM element processed this way has a __data__ property that allows D3 to bind a data item to an element.
After you've joined data, you receive the enter selection by calling enter(). This is every data element that has not yet been bound to the selected DOM elements. Typically you use the enter selection to create new elements, e.g. through append()
By calling exit() you receive the exit selection. These are all already existing DOM elements which no longer have an associated data item after the join. Typically you use the exit selection to remove DOM elements with remove()
The so called update selection is the one thing that's been returned after joining the selection with data(). You will want to store the update selection in a variable, so you have access to it even after calling enter() or exit().
Note the difference between d3v3 and d3v4:
In d3v3, when you've already added elements via the enter selection, the update selection includes those newly created DOM elements as well. It's crucial to know that the update selection changes after you've created new elements.
However, this is no longer true when using d3v4. The change log says
"In addition, selection.append no longer merges entering nodes into the update selection; use selection.merge to combine enter and update after a data join."
It is important to know that there are three different operations you can perform after binding data. Handle additions, deletions and modify things that did not change (or have been added just before).
Here is an example creating and manipulating a simple list: http://jsbin.com/sekuhamico/edit?html,css,js,output
var update = () => {
// bind data to list elements
// think of listWithData as a virtual representation of
// the array of list items you will later see in the
// DOM. d3.js does not handle the mapping from this
// virtual structure to the DOM for you. It is your task
// to define what is to happen with elements that are
// added, removed or updated.
var listWithData = ul.selectAll('li').data(listItems);
// handle additions
// by calling enter() on our virtual list, you get the
// subset of entries which need to be added to the DOM
// as their are not yet present there.
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// handle removal
// by calling exit() on our virtual list, you get the
// subset of entries which need to be removed from the
// DOM as they are not longer present in the virtual list.
listWithData.exit().remove();
// update existing
// acting directly on the virtual list will update any
// elements currently present in the DOM. If you would
// execute this line before calling exit(), you would
// also manipulate those items to be removed. If you
// would even call it before calling enter() you would
// miss on updating the newly added element.
listWithData.attr('class', i => i.active ? 'active' : '');
};
Be aware that in reality you probably need to add some sort of id to your items. To ensure the right items are removed and you do not get ordering issues.
Explanation
The update function knows nothing about what, or even if anything has changed. It does not know, nor care, if there are new data elements or if old ones have been removed. But both things could happen. Therefore we handle both cases by calling enter() and exit() respectively. The d3 functions enter() and exit() provides us with the subsets of list elements that should be added or removed. Finally we need to take care of changes in the existing data.
var listItems = [{ text: 1, active: false}, { text: 2, active: true}];
var ul = d3.select('#id').append('ul');
var update = () => {
var listWithData = ul.selectAll('li').data(listItems);
// add new
listWithData.enter().append('li').text(i => i.text).on('click', i => toggle(i));
// remove old
listWithData.exit().remove();
// update existing
listWithData.attr('class', i => i.active ? 'active' : '');
};
update();
$('#add').click(() => {
listItems.push({
text: listItems.length+1,
active: false
});
update();
});
var toggle = (i) => {
i.active = !i.active;
update();
};
li.active {
background-color:lightblue;
}
li {
padding: 5px;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="id"></div>
<button id="add">Add</button>
</body>
</html>

how to implement zoom in d3 graph of force directed layout and position the nodes

When I was trying out d3 force directed layout, I came across a challenge where
I want to zoom this svg. But Its quite tough for me to integrate.I want to remove the scrollers and put zoom for the graph.
http://nylen.tv/d3-process-map/graph.php
I want something like this which i can zoom,
http://cpettitt.github.io/project/dagre-d3/latest/demo/tcp-state-diagram.html
Below is the code where i integrate the graph in svg,
graph.svg = d3.select('#graph').append('svg')
.attr('width' , graph.width + graph.margin.left + graph.margin.right+500)
.attr('height', graph.height + graph.margin.top + graph.margin.bottom)
.append('g')
.attr('transform', 'translate(' + graph.margin.left + ',' + graph.margin.top + ')');
The Second link has something like this which implements zoom,
var svg = d3.select("svg"),
inner = svg.select("g");
// Set up zoom support
var zoom = d3.behavior.zoom().on("zoom", function() {
);
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.call(zoom
Below is the code I inspected from the link you provided(http://nylen.tv/d3-process-map/graph.php) from a file called script.js, it is not minified :)
obj.positionConstraints.push({
weight : c.weight,
x : c.x * graph.width,
y : c.y * graph.height
});
They are manually calculating the x & y positions as shown above. Their tick function has the following code:
for (var name in graph.data) {
var obj = graph.data[name];
obj.positionConstraints.forEach(function(c) {
var w = c.weight * e.alpha;
if (!isNaN(c.x)) {
obj.x = (c.x * w + obj.x * (1 - w));
}
if (!isNaN(c.y)) {
obj.y = (c.y * w + obj.y * (1 - w));
}
});
}

SVG path doesn't seem to orientate itself correctly

I'm attempting to generate a path around 2 circles that should follow them as I move them around. I've based this on an example that I found and built a prototype of what I'm expecting to achieve example
I've started including this in my application, but for some reason I can't seem to get the green path to draw around the correct positions, and I can't figure out why.
I've put together a code example to illustrate:
function generatePath(planet, moon, join) {
function distanceBetween(x1, y1, x2, y2) {
var a = (x2 - x1) * (x2 - x1);
var b = (y2 - y1) * (y2 - y1);
return Math.sqrt(a + b);
};
function circleYFromX(circle, x) {
return Math.sqrt(Math.pow(circle.r, 2) - Math.pow(x - circle.x, 2));
};
function calculateAngle(origin, point) {
var tan = (point.y - origin.y) / (point.x - origin.x);
var angle = Math.atan(tan) / Math.PI * 180 + 90;
if (point.x < origin.x) angle += 180;
return angle;
};
// Work out the distance between the moon and planet
var distance = distanceBetween(planet.x, planet.y, moon.x, moon.y);
var originDistance = planet.r - moon.r;
var distanceDiff = distance - originDistance;
if (distanceDiff < 1) {
distanceDiff = 1;
}
console.log(distance);
console.log(planet.r);
console.log(moon.r);
console.log(join.r);
console.log(planet.r + moon.r + 2 * join.r);
// Determine if the moon has moved out of the planet's gravitational pull
if (distance > 2 * join.r + planet.r + moon.r) {
return;
}
moon.h = 0;
moon.k = 0 - planet.r + moon.r - distanceDiff;
var triangleA = planet.r + join.r; // Side planet
var triangleB = moon.r + join.r; // Side moon
var triangleC = Math.abs(moon.k - 0); // Side c
var triangleP = (triangleA + triangleB + triangleC) / 2; // Triangle half perimeter
var triangleArea = Math.sqrt(triangleP * (triangleP - triangleA) * (triangleP - triangleB) * (triangleP - triangleC)); // Triangle area
var triangleH;
var triangleD;
if (triangleC >= triangleA) {
var triangleH = 2 * triangleArea / triangleC; // Triangle height
var triangleD = Math.sqrt(Math.pow(triangleA, 2) - Math.pow(triangleH, 2)); // Big circle bisection of triangleC
} else {
var triangleH = 2 * triangleArea / triangleA; // Triangle height
var triangleD = Math.sqrt(Math.pow(triangleC, 2) - Math.pow(triangleH, 2)); // Small circle bisection of triangleA
}
planet.tan = triangleH / triangleD;
planet.angle = Math.atan(planet.tan);
planet.sin = Math.sin(planet.angle);
planet.intersectX = planet.sin * planet.r;
planet.cos = Math.cos(planet.angle);
planet.intersectY = planet.cos * planet.r;
join.x = 0 + planet.sin * (planet.r + join.r);
join.y = 0 - planet.cos * (planet.r + join.r);
var coord1 = {
x: -planet.intersectX,
y: -planet.intersectY
};
var coord2 = {
x: planet.intersectX,
y: -planet.intersectY
}
moon.tan = (moon.k - join.y) / (moon.h - join.x);
moon.angle = Math.atan(moon.tan);
moon.intersectX = join.x - Math.cos(moon.angle) * (join.r);
moon.intersectY = join.y - Math.sin(moon.angle) * (join.r);
// If we have any bad values then just return no path
if (isNaN(coord1.x) || isNaN(coord1.y) || isNaN(coord2.x) || isNaN(coord2.y)) {
return;
}
var pathD = "M " + coord1.x + " " + coord1.y + " A " + planet.r + " " + planet.r + " 0 1 0 " + coord2.x + " " + coord2.y;
if (join.x - join.r <= 0 && moon.k < join.y) {
var crossOverY = circleYFromX(join, 0);
pathD += "A " + join.r + " " + join.r + " 0 0 1 0 " + (join.y + crossOverY);
pathD += "m 0 -" + (crossOverY * 2);
}
pathD += "A " + join.r + " " + join.r + " 0 0 1 " + moon.intersectX + " " + moon.intersectY;
var largeArcFlag = 1;
if (join.y < moon.k) {
largeArcFlag = 0;
}
pathD += "a " + moon.r + " " + moon.r + " 0 " + largeArcFlag + " 0 " + (moon.intersectX * -2) + " 0";
if (join.x - join.r <= 0 && moon.k < join.y) {
pathD += "A " + join.r + " " + join.r + " 0 0 1 0 " + (join.y - crossOverY);
pathD += "m 0 " + (crossOverY * 2);
}
pathD += "A " + join.r + " " + join.r + " 0 0 1 " + coord1.x + " " + coord1.y;
pathD += "A " + join.r + " " + join.r + " 0 0 1 " + coord1.x + " " + coord1.y;
return pathD;
};
var container = d3.select(".planet");
var moon = d3.select(".moon");
var tempPlanet = { x: -181.77581967381693, y: -144.9613789321555, r: 152 };
var tempMoon = { x: 0, y: 0, r: 32 };
var link = { r: 7.9 };
var pathD = generatePath(tempPlanet, tempMoon, { r: 31 });
if (pathD) {
moon.append("path")
.attr("d", pathD)
.attr("transform", "translate(" + [-181.77581967381693, 144.9613789321555] + ")")
.attr("class", "gravity")
.style("fill", "none")
.style("stroke", "red")
.style("stroke-linecap", "round")
.style("stroke-width", 2);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="1680" height="523">
<g width="1680" height="523">
<g class="galaxy-main" width="1680" height="523">
<g class="planet selected" transform="translate(341,300) scale(0.5,0.5)">
<circle r="150" style="fill: rgb(72, 119, 159); stroke-dasharray: 944.477796076938px; stroke-dashoffset: 0px; stroke-width: 8px; stroke: rgb(255, 255, 255);"></circle>
<g class="moon" transform="translate(181.77581967381693,-144.96137893215555)">
<circle r="30" class="moon-circle" id="3" style="fill: rgb(72, 119, 159);"></circle>
</g>
</g>
</g>
</g>
</svg>
Instead of drawing a green fill I'm currently drawing a red outline. What you should be able to see is that the red outline correctly surrounds the larger circle (planet) but goes vertically up instead of around the smaller circle (moon).
It appears as thought we're just missing a rotation, but the original prototype I built doesn't know about a rotation, just the center of each circle. In this case this should be really simple:
Moon
var tempMoon = { x: 0, y: 0, r: 32 };
Always located at (0, 0) as this circle sits in the center of the group which the path will be appended to
Planet
var tempPlanet = { x: -181.77581967381693, y: -144.9613789321555, r: 152 };
The planet is at the center of the group, which also contains the moon group. Therefore it's location is always just an inverse translation which positions the group containing the moon
I believe the locations are correct (I've tried adding circles on the moon layer to confirm they are in the correct place - which they are). I feel that this must be somehow down to the groups but I still can't pinpoint why this isn't rendering with the correct orientation.
Unfortunately it seems that I was missing a translate and rotate in the code toward the end that I'd missed previously for some reason. Including this in made it work as expected.

fabric.js rotate a combined path

I already searched for days and tried really a lot of things to get this right.
I want to use piecharts as progress pie. I created two fabric paths, which draws the pie chart and it works as it should.
Now I want to rotate the paths at the center point, but it doesn't work. It's actually a simple rotation. The main problem is, that the rotation point depends on the ratio of the chart. I have multiple charts and if I change one, all other charts changes as well.
I combined the two paths into a group, so every piechart is a group containing two paths.
Here are two of my piecharts. Selectable true to see what is selected.
http://i.imgur.com/Q4NLsNf.png
http://i.imgur.com/N8AldM0.png
I want the selectable Rectangle to be evenly spaced out over the whole circle, so that the rotation point is exactly at the center. I don't understand why the selectable area is always the smaller part of the pie chart.
Can anybody help me out?
That's how I calculate the pie chart
for(var i = 0; i < sectorAngleArr.length; i++)
{
startAngle = endAngle;
endAngle = startAngle + sectorAngleArr[i];
x1 = parseInt(left - (mainProgRad) * Math.sin(Math.PI*startAngle / 180));
y1 = parseInt(top - (mainProgRad) * Math.cos(Math.PI*startAngle / 180));
x2 = parseInt(left - (mainProgRad) * Math.sin(Math.PI * endAngle / 180));
y2 = parseInt(top - (mainProgRad) * Math.cos(Math.PI * endAngle / 180));
And thats how I draw it
if(i == 0 && sectorAngleArr[0] <= 180)
{
pathString = "M " + (left) + "," + (top) + " L " + (x1) + "," + (y1) + " A " + mainProgRad + "," + mainProgRad + " 0 0,0 " + (x2) + "," + (y2) + " z";
var path0 = new fabric.Path(pathString);
path0.set(
{
fill:" rgba(80, 80, 220, 0.4)",
stroke:"#0000cc",
strokeWidth:"1",
});
}
else if(i == 0 && sectorAngleArr[0] > 180)
{
pathString = "M " + (left) + "," + (top) + " L " + (x1) + "," + (y1) + " A " + mainProgRad + "," + mainProgRad + " 0 1,0 " + (x2) + "," + (y2) + " z";
var path0 = new fabric.Path(pathString);
path0.set(
{
fill:" rgba(80, 80, 220, 0.4)",
stroke:"#0000cc",
strokeWidth:"1",
});
}
else if(i == 1 && sectorAngleArr[1] <= 180)
{
pathString = "M " + (left) + "," + (top) + " L " + (x1) + "," + (y1) + " A " + mainProgRad + "," + mainProgRad + " 0 0,0 " + (x2) + "," + (y2) + " z";
var path1 = new fabric.Path(pathString);
path1.set(
{
fill:" rgba(220, 80, 80, 0.4)",
stroke:"#cc00cc",
strokeWidth:"1",
});
}
else
{
pathString = "M " + (left) + "," + (top) + " L " + (x1) + "," + (y1) + " A " + mainProgRad + "," + mainProgRad + " 0 1,0 " + (x2) + "," + (y2) + " z";
var path1 = new fabric.Path(pathString);
path1.set(
{
fill:" rgba(220, 80, 80, 0.4)",
stroke:"#cc00cc",
strokeWidth:"1",
});
}
}
var progressGroup = new fabric.Group([path0, path1],
{
left: left,
top: top,
originX: "center",
originY: "center",
scaleX: -1,
selectable:true
});
all.add(progressGroup);
I hope you can help me out!
EDIT: One good step forward was to use fabric.Pathgroup instead of fabric.Group...it reacts more as expected. But its still not working :)
OK, I find a workaround. The problem was, that the arc was ignored and the selecable area was around the 2 lines which were drawn. So I created more pieces of the pie to get the selectable area surrounding the wohle pie chart and the center, was now the center
You can see it here
http://i.imgur.com/nT7Es3O.png
EDIT: After getting some problems with an odd amount of pieces I made it just easy and drew a rectangle behind the pie chart and made it invisible. It is necessary that the rectangle has an absolute position. Now everthing works fine! :)

Categories