[Chartjs]-Calculate and plot Ellipses on scatter plot. Working but right?

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>

Leave a comment