I'm working on a predator-prey model for a dynamical systems book. I start by creating a dragable point for the initial condition. If I set up a second point whose coordinates are functions of the first point, I can drag one and it moves the other. I'm trying to get a 100 point orbit of the system, and I'm having difficulty. Here's a fiddle that works for a single point - https://jsfiddle.net/jford1906/gx86vbtc/21/
var board = JXG.JSXGraph.initBoard('jxgbox', {
boundingbox: [-0.1, 3, 1, -0.1],
axis: true,
grid: true,
showFullscreen: true
});
var p1 = board.create('point', [0.5, 0.5], {
name: 'A'
});
var coords = board.create('text',
[0.3, 2.8, function() {
return "Initial Condition: (" + JXG.toFixed(p1.X(), 2) + "," + JXG.toFixed(p1.Y(), 2) + ")";
}]
);
var p2 = board.create('point', [function() {
return 2 * p1.X() * (1 - p1.X()) - 0.5 * p1.X() * p1.Y()
}, function() {
return 4 * p1.Y() / 5 + 1.5 * p1.X() * p1.Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 3
});
What I've tried so far is to plug each point of the orbit into an array, and have the next point run the same functions for it's coordinates as I did in the working example. It initially shows the whole orbit, but when I move the initial condition, all points except the last one in the orbit vanish. This fiddle shows how I've tried to do it - https://jsfiddle.net/jford1906/jra9g2d3/3/
var i; //indexing variable
var pts = [p1] //Put the initial condition in an array
for (i = 1; i < 100; i++) {
var p2 = board.create('point', [function() {
return 2 * pts[i - 1].X() * (1 - pts[i - 1].X()) - 0.5 * pts[i - 1].X() * pts[i - 1].Y()
}, function() {
return 4 * pts[i - 1].Y() / 5 + 1.5 * pts[i - 1].X() * pts[i - 1].Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 1
});
pts.push(p2);
}
Ideas on why this might happen, or thoughts on a different approach? I've also tried putting the whole loop in a function and having that trigger when the point is dragged, but the same issue occurs.
This is a problem with JavaScript closures. It has been answered in https://groups.google.com/g/jsxgraph/c/Y1y1Mbd23ZQ. A very quick fix would be to define the variable i with let instead of var:
var pts = [p1] //Put the initial condition in an array
for (let i = 1; i < 100; i++) {
var p2 = board.create('point', [function() {
return 2 * pts[i - 1].X() * (1 - pts[i - 1].X()) - 0.5 * pts[i - 1].X() * pts[i - 1].Y()
}, function() {
return 4 * pts[i - 1].Y() / 5 + 1.5 * pts[i - 1].X() * pts[i - 1].Y()
}], {
withLabel: false,
color: "blue",
opacity: 1,
size: 1
});
pts.push(p2);
}
I'm calling a Line constructor with 2 arguments (both points), start and end. Points have x and y properties. Both constructors work most of the time (Point all of the time afaik) but when I call Line.getPerpendicular (which returns a new Line), even though the correct values are being passed into the Line constructor, it's end point values are both NaN.
I isolated the points in the Line constructor and the end point initially has values but when I set this.end = endPoint, this.end is NaN for x and y.
Creating the points directly instead of first assigning them to values doesn't help.
The Line constructor works most of the time.
function line(start, end) {
console.log({ start, end });
// { start: point { x: 97, y: 299 }, end: point { x: 223, y: 341 } }
/*
this.start = start;
this.end = end;
Originally I thought this might be the problem, like I was
passing in end by reference and then it was being gc'd
*/
const ex = end.x;
const ey = end.y;
const endP = Point(ex, ey);
console.log("END POINT", endP); // point { x: 223, y: 341 }
// the Point is definitely constructed
// correctly.
this.start = Point(start.x, start.y);
this.end = endP;
console.log(this);
// line { start: point { x: 97, y: 299 }, end: point { x: NaN, y: NaN } }
}
You can reproduce by cloning https://github.com/Sjbrimley26/geometry, running npm i, and then npm run build. Then just open the built html file and check out the console.
// Edit
Here's the getPerpendicular function that's apparently causing the bug because the Line constructor works otherwise, although I don't understand why because Line is because called with the correct point values.
line.prototype.getPerpendicular = function() {
const { slope, length, center } = this;
const inv = -1 * divide(1)(slope);
const { x, y } = center;
const b = subtract(y)(inv * x);
const x0 = x - Math.floor(length);
const y0 = x0 * inv + b;
const x1 = x + Math.floor(length);
const y1 = x1 * inv + b;
console.log({ x0, y0, x1, y1 });
const start = Point(x0, y0);
const end = Point(x1, y1);
console.log({ start, end });
return Line(start, end);
/*
This doesn't work either
return Line(
Point(
x - Math.floor(length),
(x- Math.floor(length)) * inv + b
),
Point(
x + Math.floor(length),
(x + Math.floor(length)) * inv + b
)
);
*/
}
What language is this?
Have you checked to see what happens if you remove const from the 3 lines? Not knowing this language, I'm at a disadvantage here.
I assume this is a language where you don't have to declare your function's parameters? In many languages, your header should be function line(Point start, Point end).
Why didn't you initialize this.end the same way you did this.start? That is:
this.end= Point(end.x, end.y);
If I have 4 points
var x1;
var y1;
var x2;
var y2;
var x3;
var y3;
var x4;
var y4;
that make up a box. So
(x1,y1) is top left
(x2,y2) is top right
(x3,y3) is bottom left
(x4,y4) is bottom right
And then each point has a weight ranging from 0-522. How can I calculate a coordinate (tx,ty) that lies inside the box, where the point is closer to the the place that has the least weight (but taking all weights into account). So for example. if (x3,y3) has weight 0, and the others have weight 522, the (tx,ty) should be (x3,y3). If then (x2,y2) had weight like 400, then (tx,ty) should be move a little closer towards (x2,y2) from (x3,y3).
Does anyone know if there is a formula for this?
Thanks
Creating a minimum, complete, verifiable exmample
You have a little bit of a tricky problem here, but it's really quite fun. There might be better ways to solve it, but I found it most reliable to use Point and Vector data abstractions to model the problem better
I'll start with a really simple data set – the data below can be read (eg) Point D is at cartesian coordinates (1,1) with a weight of 100.
|
|
| B(0,1) #10 D(1,1) #100
|
|
| ? solve weighted average
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Here's how we'll do it
find the unweighted midpoint, m
convert each Point to a Vector of Vector(degrees, magnitude) using m as the origin
add all the Vectors together, vectorSum
divide vectorSum's magnitude by the total magnitude
convert the vector to a point, p
offset p by unweighted midpoint m
Possible JavaScript implementation
I'll go thru the pieces one at a time then there will be a complete runnable example at the bottom.
The Math.atan2, Math.cos, and Math.sin functions we'll be using return answers in radians. That's kind of a bother, so there's a couple helpers in place to work in degrees.
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
Now we'll need a way to represent our Point and Point-related functions
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
And of course the same goes for Vector – strangely enough adding Vectors together is actually easier when you convert them back to their x and y cartesian coordinates. other than that, this code is pretty straightforward
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
Lastly we'll need to represent our data above in JavaScript and create a function which calculates the weighted point. With Point and Vector by our side, this will be a piece of cake
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Runnable script
// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))
// Point
const Point = (x,y) => ({
x,
y,
add: ({x: x2, y: y2}) =>
Point(x + x2, y + y2),
sub: ({x: x2, y: y2}) =>
Point(x - x2, y - y2),
bind: f =>
f(x,y),
inspect: () =>
`Point(${x}, ${y})`
})
Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))
// Vector
const Vector = (a,m) => ({
a,
m,
scale: x =>
Vector(a, m*x),
add: v =>
Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
inspect: () =>
`Vector(${a}, ${m})`
})
Vector.zero = Vector(0,0)
Vector.unitFromPoint = ({x,y}) => Vector(atan2(y,x), 1)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))
// data
const data = [
[Point(0,0), 20],
[Point(0,1), 10],
[Point(1,1), 100],
[Point(1,0), 40],
]
// calc unweighted midpoint
const calcMidpoint = points => {
let count = points.length;
let midpoint = points.reduce((acc, [point, _]) => acc.add(point), Point.origin)
return midpoint.bind((x,y) => Point(x/count, y/count))
}
// calc weighted point
const calcWeightedMidpoint = points => {
let midpoint = calcMidpoint(points)
let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
let vectorSum = points.reduce((acc, [point, weight]) =>
acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
return Point.fromVector(vectorSum).add(midpoint)
}
console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)
Going back to our original visualization, everything looks right!
|
|
| B(0,1) #10 D(1,1) #100
|
|
| * <-- about right here
|
|
|
| A(0,0) #20 C(1,0) #40
+----------------------------------
Checking our work
Using a set of points with equal weighting, we know what the weighted midpoint should be. Let's verify that our two primary functions calcMidpoint and calcWeightedMidpoint are working correctly
const data = [
[Point(0,0), 5],
[Point(0,1), 5],
[Point(1,1), 5],
[Point(1,0), 5],
]
calcMidpoint(data)
// => Point(0.5, 0.5)
calcWeightedMidpoint(data)
// => Point(0.5, 0.5)
Great! Now we'll test to see how some other weights work too. First let's just try all the points but one with a zero weight
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Notice if we change that weight to some ridiculous number, it won't matter. Scaling of the vector is based on the point's percentage of weight. If it gets 100% of the weight, it (the point) will not pull the weighted midpoint past (the point) itself
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 0],
[Point(1,0), 1000],
]
calcWeightedMidpoint(data)
// => Point(1, 0)
Lastly, we'll verify one more set to ensure weighting is working correctly – this time we'll have two pairs of points that are equally weighted. The output is exactly what we're expecting
const data = [
[Point(0,0), 0],
[Point(0,1), 0],
[Point(1,1), 500],
[Point(1,0), 500],
]
calcWeightedMidpoint(data)
// => Point(1, 0.5)
Millions of points
Here we will create a huge point cloud of random coordinates with random weights. If points are random and things are working correctly with our function, the answer should be pretty close to Point(0,0)
const RandomWeightedPoint = () => [
Point(Math.random() * 1000 - 500, Math.random() * 1000 - 500),
Math.random() * 1000
]
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = RandomWeightedPoint()
calcWeightedMidpoint(data)
// => Point(0.008690554978970092, -0.08307212085822799)
A++
Assume w1, w2, w3, w4 are the weights.
You can start with this (pseudocode):
M = 522
a = 1
b = 1 / ( (1 - w1/M)^a + (1 - w2/M)^a + (1 - w3/M)^a + (1 - w4/M)^a )
tx = b * (x1*(1-w1/M)^a + x2*(1-w2/M)^a + x3*(1-w3/M)^a + x4*(1-w4/M)^a)
ty = b * (y1*(1-w1/M)^a + y2*(1-w2/M)^a + y3*(1-w3/M)^a + y4*(1-w4/M)^a)
This should approximate the behavior you want to accomplish. For the simplest case set a=1 and your formula will be simpler. You can adjust behavior by changing a.
Make sure you use Math.pow instead of ^ if you use Javascript.
A very simple approach is this:
Convert each point's weight to 522 minus the actual weight.
Multiply each x/y co-ordinate by its adjusted weight.
Sum all multiplied x/y co-ordinates together, and --
Divide by the total adjusted weight of all points to get your adjusted average position.
That should produce a point with a position that is biased proportionally towards the "lightest" points, as described. Assuming that weights are prefixed w, a quick snippet (followed by JSFiddle example) is:
var tx = ((522-w1)*x1 + (522-w2)*x2 + (522-w3)*x3 + (522-w4)*x4) / (2088-(w1+w2+w3+w4));
var ty = ((522-w1)*y1 + (522-w2)*y2 + (522-w3)*y3 + (522-w4)*y4) / (2088-(w1+w2+w3+w4));
JSFiddle example of this
Even though this has already been answered, I feel the one, short code snippet that shows the simplicity of calculating a weighted-average is missing:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
Now, to make this specific to your problem, you have to apply this to your points via a reducer. The reducer makes it a moving average: the value it returns represents the weights of the points it merged.
// point: { x: xCoordinate, y: yCoordinate, w: weight }
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
w: p1.w + pw.2,
}
}
Now, you can reduce any list of points to get an average coordinate and the weight it represents:
[ /* points */ ].reduce(avgPoint, { x: 0, y: 0, w: 0 })
I hope user naomik doesn't mind, but I used some of their test cases in this runnable example:
function weightedAverage(v1, w1, v2, w2) {
if (w1 === 0) return v2;
if (w2 === 0) return v1;
return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}
function avgPoint(p1, p2) {
return {
x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
y: weightedAverage(p1.y, p1.w, p2.y, p2.w),
w: p1.w + p2.w,
}
}
function getAvgPoint(arr) {
return arr.reduce(avgPoint, {
x: 0,
y: 0,
w: 0
});
}
const testCases = [
{
data: [
{ x: 0, y: 0, w: 1 },
{ x: 0, y: 1, w: 1 },
{ x: 1, y: 1, w: 1 },
{ x: 1, y: 0, w: 1 },
],
result: { x: 0.5, y: 0.5 }
},
{
data: [
{ x: 0, y: 0, w: 0 },
{ x: 0, y: 1, w: 0 },
{ x: 1, y: 1, w: 500 },
{ x: 1, y: 0, w: 500 },
],
result: { x: 1, y: 0.5 }
}
];
testCases.forEach(c => {
var expected = c.result;
var outcome = getAvgPoint(c.data);
console.log("Expected:", expected.x, ",", expected.y);
console.log("Returned:", outcome.x, ",", outcome.y);
console.log("----");
});
const rndTest = (function() {
const randomWeightedPoint = function() {
return {
x: Math.random() * 1000 - 500,
y: Math.random() * 1000 - 500,
w: Math.random() * 1000
};
};
let data = []
for (let i = 0; i < 1e6; i++)
data[i] = randomWeightedPoint()
return getAvgPoint(data);
}());
console.log("Expected: ~0 , ~0, 500000000")
console.log("Returned:", rndTest.x, ",", rndTest.y, ",", rndTest.w);
.as-console-wrapper {
min-height: 100%;
}