1๐
โ
I think your angle formula is not OK. It must have some reason, I didnโt follow the calculations, but I verified the standard formula (formula (23)):
const angle = Math.atan2(2*b, (a-c))/2;
works fine โ see the simulation bellow. The use of atan2
makes the formula valid for both a>c and a<c.
const ctx1 = document.getElementById('chart1');
const calculateEllipse = (points, xAxis, yAxis) => {
const n = points.length;
const xMean = points.reduce((sum, p) => sum + p.x, 0) / n;
const yMean = points.reduce((sum, p) => sum + p.y, 0) / n;
let a = 0;
let b = 0;
let c = 0;
points.forEach((p) => {
const xPixel = xAxis.getPixelForValue(p.x) - xAxis.getPixelForValue(xMean);
const yPixel = yAxis.getPixelForValue(p.y) - yAxis.getPixelForValue(yMean);
a += xPixel * xPixel;
b += xPixel * yPixel;
c += yPixel * yPixel;
});
a /= n;
b /= n;
c /= n;
const d = Math.sqrt((a - c) * (a - c) + 4 * b * b);
const e1 = (a + c + d) / 2;
const e2 = (a + c - d) / 2;
//const angle = Math.PI/2 +(a > c ? Math.atan2(b, a - e1) : Math.atan2(c - e1, b));
const angle = Math.atan2(2*b, (a-c))/2;
//console.log(a>c, angle > 0)
const scaleFactor = 2.4477; // Scaling factor for a 95% confidence ellipse
return {
x: xAxis.getPixelForValue(xMean),
y: yAxis.getPixelForValue(yMean),
a: Math.sqrt(e1) * scaleFactor,
b: Math.sqrt(e2) * scaleFactor,
angle,
};
};
const ellipsePlugin = {
id: "ellipse",
afterDatasetsDraw: function (chart, args, options) {
const ctx = chart.ctx;
const xAxis = chart.scales.x;
const yAxis = chart.scales.y;
if (true) {
chart.data.datasets.forEach((dataset, index) => {
if (chart.isDatasetVisible(index)) {
const ellipseData = calculateEllipse(dataset.data, xAxis, yAxis);
ctx.save();
if (true
// ellipseData.x - ellipseData.a <= chartArea.right &&
// ellipseData.x + ellipseData.a >= chartArea.left &&
// ellipseData.y - ellipseData.b <= chartArea.bottom &&
// ellipseData.y + ellipseData.b >= chartArea.top
) {
// draw only if the ellipse is completely inside the chart area
ctx.beginPath();
ctx.translate(ellipseData.x, ellipseData.y);
ctx.rotate(ellipseData.angle);
ctx.scale(ellipseData.a, ellipseData.b);
ctx.arc(0, 0, 1, 0, 2 * Math.PI);
ctx.restore();
ctx.strokeStyle = dataset.borderColor;
ctx.lineWidth = 2;
ctx.stroke();
}
}
});
}
},
};
Chart.register(ellipsePlugin);
let a, b, theta = 0;
const data = Array(2000).fill(null);
function updateData(){
for(let i = 0; i < data.length; i++){
const f = Math.random(), phi = Math.random() * Math.PI * 2;
const x0 = a * f * Math.cos(phi), y0 = b * f * Math.sin(phi);
data[i] = {
x: x0 * Math.cos(theta) + y0 * Math.sin(theta),
y: -x0 * Math.sin(theta) + y0 * Math.cos(theta)
};
}
}
updateData();
const chart = new Chart(ctx1, {
type: 'scatter',
data: {
datasets: [
{
data
}
]
},
options: {
animation: {
duration: 0
},
scales:{
x: {
min:-10,
max:10
},
y: {
min:-10,
max:10
}
},
plugins: {
legend: {
display: false
}
}
},
plugins:[ellipsePlugin]
});
let interval, started = false;
const elAB = document.querySelector('#ab');
const plot = () => {updateData();chart.update();},
start = ()=> {
a = parseFloat(document.querySelector('#a').value) || a;
b = parseFloat(document.querySelector('#b').value) || b;
theta = 0;
plot();
interval = setInterval(
() => {
theta += Math.PI / 30;
plot();
},
100
);
elAB.style.visibility = 'hidden';
started = true;
},
stop = () => {
clearInterval(interval);
elAB.style.visibility = 'visible';
started = false;
};
//start();
const butStop = document.querySelector('#stop');
butStop.onclick = function(){
if(started){
stop();
butStop.innerHTML = 'Start';
}
else{
start();
butStop.innerHTML = 'Stop';
}
}
<button id="stop">Start</button>
<span id="ab">
<label>a = <input type="text" id="a" value="4" size="3"></label>
<label>b = <input type="text" id="b" value="7" size="3"></label>
</span>
<canvas id="chart1" style="width:400px;height:400px"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.1.2/chart.umd.js"
integrity="sha512-t41WshQCxr9T3SWH3DBZoDnAT9gfVLtQS+NKO60fdAwScoB37rXtdxT/oKe986G0BFnP4mtGzXxuYpHrMoMJLA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Source:stackexchange.com